uncertain_rs/
distributions.rs

1#![allow(clippy::cast_precision_loss)]
2
3use crate::Uncertain;
4use crate::traits::Shareable;
5use rand::prelude::*;
6use rand::random;
7use rand::rng;
8use std::collections::HashMap;
9use std::f64::consts::PI;
10
11impl<T> Uncertain<T>
12where
13    T: Shareable,
14{
15    /// Creates a point-mass distribution (certain value)
16    ///
17    /// # Example
18    /// ```rust
19    /// use uncertain_rs::Uncertain;
20    ///
21    /// let certain_value = Uncertain::point(42.0);
22    /// assert_eq!(certain_value.sample(), 42.0);
23    /// ```
24    #[must_use]
25    pub fn point(value: T) -> Self {
26        Uncertain::new(move || value.clone())
27    }
28
29    /// Creates a mixture of distributions with optional weights
30    ///
31    /// # Arguments
32    /// * `components` - Vector of distributions to mix
33    /// * `weights` - Optional weights for each component (uniform if None)
34    ///
35    /// # Errors
36    /// Returns an error if the components vector is empty or if the weights count
37    /// doesn't match the components count.
38    ///
39    /// # Panics
40    /// May panic if the components vector is empty when attempting to get the first
41    /// component, which should not happen due to the input validation.
42    ///
43    /// # Example
44    /// ```rust
45    /// use uncertain_rs::Uncertain;
46    ///
47    /// let normal1 = Uncertain::normal(0.0, 1.0);
48    /// let normal2 = Uncertain::normal(5.0, 1.0);
49    /// let mixture = Uncertain::mixture(
50    ///     vec![normal1, normal2],
51    ///     Some(vec![0.7, 0.3])
52    /// ).unwrap();
53    /// ```
54    pub fn mixture(
55        components: Vec<Uncertain<T>>,
56        weights: Option<Vec<f64>>,
57    ) -> Result<Self, &'static str> {
58        if components.is_empty() {
59            return Err("At least one component required");
60        }
61
62        if components.len() == 1 {
63            return Ok(components.into_iter().next().unwrap());
64        }
65
66        let weights = match weights {
67            Some(w) => {
68                if w.len() != components.len() {
69                    return Err("Weights count must match components count");
70                }
71                w
72            }
73            None => vec![1.0; components.len()],
74        };
75
76        let total: f64 = weights.iter().sum();
77        let normalized: Vec<f64> = weights.iter().map(|&w| w / total).collect();
78        let cumulative: Vec<f64> = normalized
79            .iter()
80            .scan(0.0, |acc, &x| {
81                *acc += x;
82                Some(*acc)
83            })
84            .collect();
85
86        Ok(Uncertain::new(move || {
87            let r: f64 = random();
88            let idx = cumulative
89                .iter()
90                .position(|&x| r <= x)
91                .unwrap_or_else(|| components.len().saturating_sub(1));
92            components.get(idx).map_or_else(
93                || components[0].sample(),
94                super::uncertain::Uncertain::sample,
95            )
96        }))
97    }
98
99    /// Creates an empirical distribution from observed data
100    ///
101    /// # Arguments
102    /// * `data` - Vector of observed data points
103    ///
104    /// # Errors
105    /// Returns an error if the data vector is empty.
106    ///
107    /// # Panics
108    /// May panic if the random number generator fails to select from the data,
109    /// which should not happen if the data is non-empty.
110    ///
111    /// # Example
112    /// ```rust
113    /// use uncertain_rs::Uncertain;
114    ///
115    /// let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
116    /// let empirical = Uncertain::empirical(data).unwrap();
117    /// ```
118    pub fn empirical(data: Vec<T>) -> Result<Self, &'static str> {
119        if data.is_empty() {
120            return Err("Data cannot be empty");
121        }
122
123        Ok(Uncertain::new(move || {
124            data.choose(&mut rng())
125                .expect("Data vector should not be empty")
126                .clone()
127        }))
128    }
129}
130
131// Categorical distribution for hashable types
132impl<T> Uncertain<T>
133where
134    T: Clone + Send + Sync + std::hash::Hash + Eq + 'static,
135{
136    /// Creates a categorical distribution from value-probability pairs
137    ///
138    /// # Arguments
139    /// * `probabilities` - A map of values to their probabilities
140    ///
141    /// # Errors
142    /// Returns an error if the probabilities map is empty.
143    ///
144    /// # Panics
145    /// May panic if the cumulative probability vector is empty, which should not happen
146    /// if the input validation passes.
147    ///
148    /// # Example
149    /// ```rust
150    /// use uncertain_rs::Uncertain;
151    /// use std::collections::HashMap;
152    ///
153    /// let mut probs = HashMap::new();
154    /// probs.insert("red", 0.5);
155    /// probs.insert("blue", 0.3);
156    /// probs.insert("green", 0.2);
157    ///
158    /// let color = Uncertain::categorical(&probs).unwrap();
159    /// ```
160    pub fn categorical(probabilities: &HashMap<T, f64>) -> Result<Self, &'static str> {
161        if probabilities.is_empty() {
162            return Err("Probabilities cannot be empty");
163        }
164
165        let total: f64 = probabilities.values().sum();
166        let mut cumulative = Vec::new();
167        let mut sum = 0.0;
168
169        for (value, &prob) in probabilities {
170            sum += prob / total;
171            cumulative.push((value.clone(), sum));
172        }
173
174        Ok(Uncertain::new(move || {
175            let r: f64 = random();
176            cumulative.iter().find(|(_, cum)| r <= *cum).map_or_else(
177                || {
178                    cumulative
179                        .last()
180                        .expect("Cumulative vector should not be empty")
181                        .0
182                        .clone()
183                },
184                |(val, _)| val.clone(),
185            )
186        }))
187    }
188}
189
190// Floating point distributions
191impl Uncertain<f64> {
192    /// Creates a normal (Gaussian) distribution
193    ///
194    /// # Arguments
195    /// * `mean` - The mean of the distribution
196    /// * `std_dev` - The standard deviation
197    ///
198    /// # Example
199    /// ```rust
200    /// use uncertain_rs::Uncertain;
201    ///
202    /// let normal = Uncertain::normal(0.0, 1.0); // Standard normal
203    /// let measurement = Uncertain::normal(100.0, 5.0); // Measurement with error
204    /// ```
205    #[must_use]
206    pub fn normal(mean: f64, std_dev: f64) -> Self {
207        Uncertain::new(move || {
208            // Box-Muller transform for normal distribution
209            let u1: f64 = random::<f64>().clamp(0.001, 0.999);
210            let u2: f64 = random::<f64>().clamp(0.001, 0.999);
211            let z0 = (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos();
212            mean + std_dev * z0
213        })
214    }
215
216    /// Creates a uniform distribution
217    ///
218    /// # Example
219    /// ```rust
220    /// use uncertain_rs::Uncertain;
221    ///
222    /// let uniform = Uncertain::uniform(0.0, 10.0);
223    /// ```
224    #[must_use]
225    pub fn uniform(min: f64, max: f64) -> Self {
226        Uncertain::new(move || min + (max - min) * random::<f64>())
227    }
228
229    /// Creates an exponential distribution
230    ///
231    /// # Arguments
232    /// * `rate` - The rate parameter (lambda)
233    ///
234    /// # Example
235    /// ```rust
236    /// use uncertain_rs::Uncertain;
237    ///
238    /// let exponential = Uncertain::exponential(1.0);
239    /// ```
240    #[must_use]
241    pub fn exponential(rate: f64) -> Self {
242        Uncertain::new(move || -random::<f64>().ln() / rate)
243    }
244
245    /// Creates a log-normal distribution
246    ///
247    /// # Arguments
248    /// * `mu` - Mean of the underlying normal distribution
249    /// * `sigma` - Standard deviation of the underlying normal distribution
250    ///
251    /// # Example
252    /// ```rust
253    /// use uncertain_rs::Uncertain;
254    ///
255    /// let lognormal = Uncertain::log_normal(0.0, 1.0);
256    /// ```
257    #[must_use]
258    pub fn log_normal(mu: f64, sigma: f64) -> Self {
259        let normal = Self::normal(mu, sigma);
260        normal.map(f64::exp)
261    }
262
263    /// Creates a beta distribution
264    ///
265    /// # Arguments
266    /// * `alpha` - First shape parameter
267    /// * `beta` - Second shape parameter
268    ///
269    /// # Example
270    /// ```rust
271    /// use uncertain_rs::Uncertain;
272    ///
273    /// let beta = Uncertain::beta(2.0, 5.0);
274    /// ```
275    #[must_use]
276    pub fn beta(alpha: f64, beta: f64) -> Self {
277        Uncertain::new(move || {
278            // Using rejection sampling method
279            loop {
280                let u1: f64 = random();
281                let u2: f64 = random();
282
283                let x = u1.powf(1.0 / alpha);
284                let y = u2.powf(1.0 / beta);
285
286                if x + y <= 1.0 {
287                    return x / (x + y);
288                }
289            }
290        })
291    }
292
293    /// Creates a gamma distribution
294    ///
295    /// # Arguments
296    /// * `shape` - Shape parameter (alpha)
297    /// * `scale` - Scale parameter (beta)
298    ///
299    /// # Example
300    /// ```rust
301    /// use uncertain_rs::Uncertain;
302    ///
303    /// let gamma = Uncertain::gamma(2.0, 1.0);
304    /// ```
305    #[must_use]
306    pub fn gamma(shape: f64, scale: f64) -> Self {
307        Uncertain::new(move || {
308            // Marsaglia and Tsang method for shape >= 1
309            if shape >= 1.0 {
310                let d = shape - 1.0 / 3.0;
311                let c = 1.0 / (9.0 * d).sqrt();
312
313                loop {
314                    let normal_sample = Self::normal(0.0, 1.0).sample();
315                    let v = (1.0 + c * normal_sample).powi(3);
316
317                    if v > 0.0 {
318                        let u: f64 = random();
319                        if u < 1.0 - 0.0331 * normal_sample.powi(4) {
320                            return d * v * scale;
321                        }
322                        if u.ln() < 0.5 * normal_sample.powi(2) + d * (1.0 - v + v.ln()) {
323                            return d * v * scale;
324                        }
325                    }
326                }
327            } else {
328                // For shape < 1, use transformation
329                let gamma_1_plus_shape = Self::gamma(shape + 1.0, scale).sample();
330                let u: f64 = random();
331                gamma_1_plus_shape * u.powf(1.0 / shape)
332            }
333        })
334    }
335}
336
337// Boolean distributions
338impl Uncertain<bool> {
339    /// Creates a Bernoulli distribution
340    ///
341    /// # Arguments
342    /// * `probability` - Probability of success (true)
343    ///
344    /// # Example
345    /// ```rust
346    /// use uncertain_rs::Uncertain;
347    ///
348    /// let biased_coin = Uncertain::bernoulli(0.7); // 70% chance of true
349    /// ```
350    #[must_use]
351    pub fn bernoulli(probability: f64) -> Self {
352        Uncertain::new(move || random::<f64>() < probability)
353    }
354}
355
356// Integer distributions
357impl<T> Uncertain<T>
358where
359    T: Clone
360        + Send
361        + Sync
362        + From<u32>
363        + std::ops::AddAssign
364        + std::ops::Sub<Output = T>
365        + Default
366        + 'static,
367{
368    /// Creates a binomial distribution
369    ///
370    /// # Arguments
371    /// * `trials` - Number of trials
372    /// * `probability` - Probability of success on each trial
373    ///
374    /// # Example
375    /// ```rust
376    /// use uncertain_rs::Uncertain;
377    ///
378    /// let binomial: Uncertain<u32> = Uncertain::binomial(100, 0.3);
379    /// ```
380    #[must_use]
381    pub fn binomial(trials: u32, probability: f64) -> Self {
382        Uncertain::new(move || {
383            let mut count = T::default();
384            for _ in 0..trials {
385                if random::<f64>() < probability {
386                    count += T::from(1);
387                }
388            }
389            count
390        })
391    }
392
393    /// Creates a Poisson distribution
394    ///
395    /// # Arguments
396    /// * `lambda` - Rate parameter
397    ///
398    /// # Example
399    /// ```rust
400    /// use uncertain_rs::Uncertain;
401    ///
402    /// let poisson: Uncertain<u32> = Uncertain::poisson(3.5);
403    /// ```
404    #[must_use]
405    pub fn poisson(lambda: f64) -> Self {
406        Uncertain::new(move || {
407            let l = (-lambda).exp();
408            let mut k = T::from(0);
409            let mut p = 1.0;
410
411            loop {
412                k += T::from(1);
413                p *= random::<f64>();
414                if p <= l {
415                    break;
416                }
417            }
418
419            // Return k - 1 per Knuth's algorithm
420            k - T::from(1)
421        })
422    }
423
424    /// Creates a geometric distribution
425    ///
426    /// # Arguments
427    /// * `probability` - Probability of success on each trial
428    ///
429    /// # Example
430    /// ```rust
431    /// use uncertain_rs::Uncertain;
432    ///
433    /// let geometric: Uncertain<u32> = Uncertain::geometric(0.1);
434    /// ```
435    #[must_use]
436    pub fn geometric(probability: f64) -> Self {
437        Uncertain::new(move || {
438            let mut trials = T::from(1);
439            while random::<f64>() >= probability {
440                trials += T::from(1);
441            }
442            trials
443        })
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    #[test]
452    fn test_normal_distribution() {
453        let normal = Uncertain::normal(0.0, 1.0);
454        let samples: Vec<f64> = normal.take_samples(1000);
455        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
456        assert!((mean - 0.0).abs() < 0.1);
457    }
458
459    #[test]
460    fn test_uniform_distribution() {
461        let uniform = Uncertain::uniform(0.0, 10.0);
462        let samples: Vec<f64> = uniform.take_samples(1000);
463        assert!(samples.iter().all(|&x| (0.0..=10.0).contains(&x)));
464    }
465
466    #[test]
467    fn test_bernoulli_distribution() {
468        let bernoulli = Uncertain::bernoulli(0.7);
469        let samples: Vec<bool> = bernoulli.take_samples(1000);
470        let true_ratio = samples.iter().filter(|&&x| x).count() as f64 / samples.len() as f64;
471        assert!((true_ratio - 0.7).abs() < 0.1);
472    }
473
474    #[test]
475    #[allow(clippy::float_cmp)]
476    fn test_point_mass() {
477        let point = Uncertain::point(42.0);
478        for _ in 0..10 {
479            assert_eq!(point.sample(), 42.0);
480        }
481    }
482
483    #[test]
484    fn test_categorical() {
485        let mut probs = HashMap::new();
486        probs.insert("a", 0.5);
487        probs.insert("b", 0.3);
488        probs.insert("c", 0.2);
489
490        let categorical = Uncertain::categorical(&probs).unwrap();
491        let samples: Vec<&str> = categorical.take_samples(1000);
492
493        assert!(samples.iter().all(|&x| ["a", "b", "c"].contains(&x)));
494    }
495
496    #[test]
497    fn test_exponential_distribution() {
498        let exponential = Uncertain::exponential(2.0);
499        let samples: Vec<f64> = exponential.take_samples(1000);
500
501        assert!(samples.iter().all(|&x| x >= 0.0));
502
503        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
504        assert!((mean - 0.5).abs() < 0.1);
505    }
506
507    #[test]
508    fn test_log_normal_distribution() {
509        let log_normal = Uncertain::log_normal(0.0, 1.0);
510        let samples: Vec<f64> = log_normal.take_samples(1000);
511
512        assert!(samples.iter().all(|&x| x > 0.0));
513    }
514
515    #[test]
516    fn test_beta_distribution() {
517        let beta = Uncertain::beta(2.0, 5.0);
518        let samples: Vec<f64> = beta.take_samples(1000);
519
520        assert!(samples.iter().all(|&x| (0.0..=1.0).contains(&x)));
521    }
522
523    #[test]
524    fn test_gamma_distribution() {
525        let gamma = Uncertain::gamma(2.0, 1.0);
526        let samples: Vec<f64> = gamma.take_samples(1000);
527
528        assert!(samples.iter().all(|&x| x >= 0.0));
529    }
530
531    #[test]
532    fn test_binomial_distribution() {
533        let binomial: Uncertain<u32> = Uncertain::binomial(10, 0.5);
534        let samples: Vec<u32> = binomial.take_samples(1000);
535
536        assert!(samples.iter().all(|&x| x <= 10));
537
538        let mean = f64::from(samples.iter().sum::<u32>()) / samples.len() as f64;
539        assert!((mean - 5.0).abs() < 1.0);
540    }
541
542    #[test]
543    fn test_poisson_distribution() {
544        let poisson: Uncertain<u32> = Uncertain::poisson(3.0);
545        let samples: Vec<u32> = poisson.take_samples(1000);
546
547        let mean = f64::from(samples.iter().sum::<u32>()) / samples.len() as f64;
548        assert!((mean - 3.0).abs() < 1.0);
549    }
550
551    #[test]
552    fn test_geometric_distribution() {
553        let geometric: Uncertain<u32> = Uncertain::geometric(0.2);
554        let samples: Vec<u32> = geometric.take_samples(1000);
555
556        assert!(samples.iter().all(|&x| x >= 1));
557
558        let mean = f64::from(samples.iter().sum::<u32>()) / samples.len() as f64;
559        assert!((mean - 5.0).abs() < 2.0);
560    }
561
562    #[test]
563    fn test_mixture_distribution() {
564        let normal1 = Uncertain::normal(0.0, 1.0);
565        let normal2 = Uncertain::normal(10.0, 1.0);
566        let mixture = Uncertain::mixture(vec![normal1, normal2], Some(vec![0.5, 0.5])).unwrap();
567
568        let samples: Vec<f64> = mixture.take_samples(1000);
569
570        let low_count = samples.iter().filter(|&&x| x < 5.0).count();
571        let high_count = samples.iter().filter(|&&x| x > 5.0).count();
572
573        assert!(low_count > 100);
574        assert!(high_count > 100);
575    }
576
577    #[test]
578    fn test_mixture_single_component() {
579        let normal = Uncertain::normal(0.0, 1.0);
580        let mixture = Uncertain::mixture(vec![normal], None).unwrap();
581        let samples: Vec<f64> = mixture.take_samples(100);
582        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
583        assert!((mean - 0.0).abs() < 0.5);
584    }
585
586    #[test]
587    fn test_mixture_uniform_weights() {
588        let normal1 = Uncertain::normal(0.0, 1.0);
589        let normal2 = Uncertain::normal(5.0, 1.0);
590        let mixture = Uncertain::mixture(vec![normal1, normal2], None).unwrap();
591        let _samples: Vec<f64> = mixture.take_samples(100);
592    }
593
594    #[test]
595    fn test_empirical_distribution() {
596        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
597        let empirical = Uncertain::empirical(data.clone()).unwrap();
598        let samples: Vec<f64> = empirical.take_samples(1000);
599
600        assert!(samples.iter().all(|&x| data.contains(&x)));
601    }
602
603    #[test]
604    fn test_mixture_empty_components() {
605        let result: Result<Uncertain<f64>, &str> = Uncertain::mixture(vec![], None);
606        assert!(result.is_err());
607        assert_eq!(result.unwrap_err(), "At least one component required");
608    }
609
610    #[test]
611    fn test_mixture_mismatched_weights() {
612        let normal1 = Uncertain::normal(0.0, 1.0);
613        let normal2 = Uncertain::normal(1.0, 1.0);
614        let result = Uncertain::mixture(vec![normal1, normal2], Some(vec![0.5]));
615        assert!(result.is_err());
616        assert_eq!(
617            result.unwrap_err(),
618            "Weights count must match components count"
619        );
620    }
621
622    #[test]
623    fn test_empirical_empty_data() {
624        let result: Result<Uncertain<f64>, &str> = Uncertain::empirical(vec![]);
625        assert!(result.is_err());
626        assert_eq!(result.unwrap_err(), "Data cannot be empty");
627    }
628
629    #[test]
630    fn test_categorical_empty_probabilities() {
631        let probs: HashMap<&str, f64> = HashMap::new();
632        let result = Uncertain::categorical(&probs);
633        assert!(result.is_err());
634        assert_eq!(result.unwrap_err(), "Probabilities cannot be empty");
635    }
636
637    #[test]
638    fn test_categorical_with_unnormalized_probabilities() {
639        let mut probs = HashMap::new();
640        probs.insert("a", 2.0);
641        probs.insert("b", 3.0);
642        probs.insert("c", 5.0);
643
644        let categorical = Uncertain::categorical(&probs).unwrap();
645        let samples: Vec<&str> = categorical.take_samples(1000);
646
647        assert!(samples.iter().all(|&x| ["a", "b", "c"].contains(&x)));
648    }
649
650    #[test]
651    fn test_normal_distribution_edge_cases() {
652        let normal_zero_std = Uncertain::normal(5.0, 0.0);
653        let samples: Vec<f64> = normal_zero_std.take_samples(10);
654        assert!(samples.iter().all(|&x| (x - 5.0).abs() < 0.01));
655
656        let normal_negative = Uncertain::normal(-10.0, 2.0);
657        let samples: Vec<f64> = normal_negative.take_samples(1000);
658        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
659        assert!((mean - (-10.0)).abs() < 0.5);
660    }
661
662    #[test]
663    fn test_uniform_edge_cases() {
664        let uniform_point = Uncertain::uniform(5.0, 5.0);
665        let samples: Vec<f64> = uniform_point.take_samples(10);
666        assert!(samples.iter().all(|&x| (x - 5.0).abs() < f64::EPSILON));
667
668        let uniform_negative = Uncertain::uniform(-10.0, -5.0);
669        let samples: Vec<f64> = uniform_negative.take_samples(100);
670        assert!(samples.iter().all(|&x| (-10.0..=-5.0).contains(&x)));
671    }
672
673    #[test]
674    fn test_bernoulli_edge_cases() {
675        let bernoulli_false = Uncertain::bernoulli(0.0);
676        let samples: Vec<bool> = bernoulli_false.take_samples(100);
677        assert!(samples.iter().all(|&x| !x));
678
679        let bernoulli_true = Uncertain::bernoulli(1.0);
680        let samples: Vec<bool> = bernoulli_true.take_samples(100);
681        assert!(samples.iter().all(|&x| x));
682    }
683
684    #[test]
685    fn test_exponential_edge_cases() {
686        let exponential_high_rate = Uncertain::exponential(100.0);
687        let samples: Vec<f64> = exponential_high_rate.take_samples(100);
688        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
689        assert!(mean < 0.1);
690    }
691
692    #[test]
693    fn test_binomial_edge_cases() {
694        let binomial_zero: Uncertain<u32> = Uncertain::binomial(0, 0.5);
695        let samples: Vec<u32> = binomial_zero.take_samples(10);
696        assert!(samples.iter().all(|&x| x == 0));
697
698        let binomial_p_zero: Uncertain<u32> = Uncertain::binomial(10, 0.0);
699        let samples: Vec<u32> = binomial_p_zero.take_samples(10);
700        assert!(samples.iter().all(|&x| x == 0));
701
702        let binomial_p_one: Uncertain<u32> = Uncertain::binomial(10, 1.0);
703        let samples: Vec<u32> = binomial_p_one.take_samples(10);
704        assert!(samples.iter().all(|&x| x == 10));
705    }
706}