Skip to main content

proof_engine/stochastic/
poisson.rs

1//! Poisson processes: homogeneous, non-homogeneous, and compound.
2//!
3//! Models event arrivals where events occur independently at a given rate.
4
5use super::brownian::Rng;
6
7// ---------------------------------------------------------------------------
8// PoissonProcess (homogeneous)
9// ---------------------------------------------------------------------------
10
11/// Homogeneous Poisson process with constant rate lambda.
12pub struct PoissonProcess {
13    /// Events per unit time.
14    pub rate: f64,
15}
16
17impl PoissonProcess {
18    pub fn new(rate: f64) -> Self {
19        assert!(rate > 0.0, "rate must be positive");
20        Self { rate }
21    }
22
23    /// Generate the next inter-arrival time (exponential distribution).
24    /// T ~ Exp(rate) => T = -ln(U) / rate
25    pub fn next_arrival(&self, rng: &mut Rng) -> f64 {
26        let u = rng.uniform().max(1e-15);
27        -u.ln() / self.rate
28    }
29
30    /// Generate all arrival times in [0, duration].
31    pub fn arrivals(&self, rng: &mut Rng, duration: f64) -> Vec<f64> {
32        let mut times = Vec::new();
33        let mut t = 0.0;
34        loop {
35            t += self.next_arrival(rng);
36            if t > duration {
37                break;
38            }
39            times.push(t);
40        }
41        times
42    }
43
44    /// Count distribution: N(t) ~ Poisson(rate * t).
45    pub fn count(&self, duration: f64) -> PoissonDistribution {
46        PoissonDistribution {
47            lambda: self.rate * duration,
48        }
49    }
50
51    /// Generate a counting process path: (time, count) pairs.
52    pub fn counting_path(&self, rng: &mut Rng, duration: f64) -> Vec<(f64, usize)> {
53        let arrivals = self.arrivals(rng, duration);
54        let mut path = Vec::with_capacity(arrivals.len() + 2);
55        path.push((0.0, 0));
56        for (i, &t) in arrivals.iter().enumerate() {
57            path.push((t, i + 1));
58        }
59        path.push((duration, arrivals.len()));
60        path
61    }
62
63    /// Superposition of two independent Poisson processes.
64    pub fn superpose(&self, other: &PoissonProcess) -> PoissonProcess {
65        PoissonProcess::new(self.rate + other.rate)
66    }
67
68    /// Thin this process with probability p to get rate * p.
69    pub fn thin(&self, p: f64) -> PoissonProcess {
70        assert!(p > 0.0 && p <= 1.0);
71        PoissonProcess::new(self.rate * p)
72    }
73}
74
75// ---------------------------------------------------------------------------
76// PoissonDistribution
77// ---------------------------------------------------------------------------
78
79/// Poisson distribution with parameter lambda.
80pub struct PoissonDistribution {
81    pub lambda: f64,
82}
83
84impl PoissonDistribution {
85    pub fn new(lambda: f64) -> Self {
86        Self { lambda }
87    }
88
89    /// P(N = k) = e^{-lambda} * lambda^k / k!
90    pub fn pmf(&self, k: usize) -> f64 {
91        (-self.lambda).exp() * self.lambda.powi(k as i32) / factorial(k) as f64
92    }
93
94    /// P(N <= k)
95    pub fn cdf(&self, k: usize) -> f64 {
96        (0..=k).map(|i| self.pmf(i)).sum()
97    }
98
99    /// Expected value: lambda.
100    pub fn mean(&self) -> f64 {
101        self.lambda
102    }
103
104    /// Variance: lambda.
105    pub fn variance(&self) -> f64 {
106        self.lambda
107    }
108
109    /// Sample a Poisson random variable using the inverse transform method.
110    pub fn sample(&self, rng: &mut Rng) -> usize {
111        // Knuth algorithm
112        let l = (-self.lambda).exp();
113        let mut k = 0usize;
114        let mut p = 1.0;
115        loop {
116            k += 1;
117            p *= rng.uniform();
118            if p <= l {
119                return k - 1;
120            }
121        }
122    }
123}
124
125fn factorial(n: usize) -> f64 {
126    if n <= 1 {
127        1.0
128    } else {
129        (2..=n).fold(1.0, |acc, i| acc * i as f64)
130    }
131}
132
133// ---------------------------------------------------------------------------
134// NonHomogeneousPoisson
135// ---------------------------------------------------------------------------
136
137/// Non-homogeneous Poisson process with time-varying rate function lambda(t).
138pub struct NonHomogeneousPoisson {
139    /// Rate function lambda(t).
140    pub rate_fn: Box<dyn Fn(f64) -> f64>,
141    /// Upper bound on the rate function (for thinning algorithm).
142    pub rate_max: f64,
143}
144
145impl NonHomogeneousPoisson {
146    pub fn new(rate_fn: Box<dyn Fn(f64) -> f64>, rate_max: f64) -> Self {
147        Self { rate_fn, rate_max }
148    }
149
150    /// Generate arrival times using the thinning (Lewis-Shedler) algorithm.
151    ///
152    /// 1. Generate candidate arrival from homogeneous Poisson(rate_max)
153    /// 2. Accept with probability lambda(t) / rate_max
154    pub fn arrivals(&self, rng: &mut Rng, duration: f64) -> Vec<f64> {
155        let mut times = Vec::new();
156        let mut t = 0.0;
157        let homogeneous = PoissonProcess::new(self.rate_max);
158
159        loop {
160            t += homogeneous.next_arrival(rng);
161            if t > duration {
162                break;
163            }
164            // Accept/reject
165            let acceptance_prob = (self.rate_fn)(t) / self.rate_max;
166            if rng.uniform() < acceptance_prob {
167                times.push(t);
168            }
169        }
170        times
171    }
172
173    /// Generate counting process path.
174    pub fn counting_path(&self, rng: &mut Rng, duration: f64) -> Vec<(f64, usize)> {
175        let arrivals = self.arrivals(rng, duration);
176        let mut path = Vec::with_capacity(arrivals.len() + 2);
177        path.push((0.0, 0));
178        for (i, &t) in arrivals.iter().enumerate() {
179            path.push((t, i + 1));
180        }
181        path.push((duration, arrivals.len()));
182        path
183    }
184
185    /// Expected number of arrivals in [0, t] = integral of lambda(s) ds.
186    /// Computed via simple trapezoidal rule.
187    pub fn expected_count(&self, duration: f64, n_points: usize) -> f64 {
188        let dx = duration / n_points as f64;
189        let mut sum = 0.5 * ((self.rate_fn)(0.0) + (self.rate_fn)(duration));
190        for i in 1..n_points {
191            sum += (self.rate_fn)(i as f64 * dx);
192        }
193        sum * dx
194    }
195}
196
197// ---------------------------------------------------------------------------
198// CompoundPoisson
199// ---------------------------------------------------------------------------
200
201/// Compound Poisson process: S(t) = sum_{i=1}^{N(t)} X_i
202/// where N(t) is Poisson(rate*t) and X_i are iid from jump_distribution.
203pub struct CompoundPoisson {
204    /// Rate of underlying Poisson process.
205    pub rate: f64,
206    /// Jump size distribution: function that generates a random jump.
207    pub jump_distribution: Box<dyn Fn(&mut Rng) -> f64>,
208}
209
210impl CompoundPoisson {
211    pub fn new(rate: f64, jump_distribution: Box<dyn Fn(&mut Rng) -> f64>) -> Self {
212        Self { rate, jump_distribution }
213    }
214
215    /// Create a compound Poisson with normally distributed jumps.
216    pub fn normal_jumps(rate: f64, jump_mean: f64, jump_std: f64) -> Self {
217        Self {
218            rate,
219            jump_distribution: Box::new(move |rng: &mut Rng| rng.normal_with(jump_mean, jump_std)),
220        }
221    }
222
223    /// Create a compound Poisson with exponentially distributed jumps.
224    pub fn exponential_jumps(rate: f64, jump_rate: f64) -> Self {
225        Self {
226            rate,
227            jump_distribution: Box::new(move |rng: &mut Rng| {
228                let u = rng.uniform().max(1e-15);
229                -u.ln() / jump_rate
230            }),
231        }
232    }
233
234    /// Generate the process path: Vec of (time, cumulative_sum).
235    pub fn path(&self, rng: &mut Rng, duration: f64) -> Vec<(f64, f64)> {
236        let pp = PoissonProcess::new(self.rate);
237        let arrivals = pp.arrivals(rng, duration);
238        let mut path = Vec::with_capacity(arrivals.len() + 2);
239        path.push((0.0, 0.0));
240        let mut cumsum = 0.0;
241        for &t in &arrivals {
242            cumsum += (self.jump_distribution)(rng);
243            path.push((t, cumsum));
244        }
245        path.push((duration, cumsum));
246        path
247    }
248
249    /// Generate just the jump values at each arrival.
250    pub fn jumps(&self, rng: &mut Rng, duration: f64) -> Vec<(f64, f64)> {
251        let pp = PoissonProcess::new(self.rate);
252        let arrivals = pp.arrivals(rng, duration);
253        arrivals
254            .iter()
255            .map(|&t| (t, (self.jump_distribution)(rng)))
256            .collect()
257    }
258}
259
260// ---------------------------------------------------------------------------
261// Tests
262// ---------------------------------------------------------------------------
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_poisson_arrival_positive() {
270        let pp = PoissonProcess::new(5.0);
271        let mut rng = Rng::new(42);
272        for _ in 0..100 {
273            let t = pp.next_arrival(&mut rng);
274            assert!(t > 0.0, "inter-arrival time should be positive");
275        }
276    }
277
278    #[test]
279    fn test_poisson_mean_count() {
280        // E[N(t)] = rate * t
281        let rate = 3.0;
282        let duration = 10.0;
283        let pp = PoissonProcess::new(rate);
284        let trials = 5000;
285        let mut rng = Rng::new(42);
286
287        let total_count: usize = (0..trials)
288            .map(|_| pp.arrivals(&mut rng, duration).len())
289            .sum();
290        let empirical_mean = total_count as f64 / trials as f64;
291        let expected = rate * duration; // 30
292
293        assert!(
294            (empirical_mean - expected).abs() < 2.0,
295            "mean count {} should be ~{}",
296            empirical_mean,
297            expected
298        );
299    }
300
301    #[test]
302    fn test_poisson_inter_arrivals_exponential() {
303        // Inter-arrival times should have mean 1/rate
304        let rate = 4.0;
305        let pp = PoissonProcess::new(rate);
306        let mut rng = Rng::new(123);
307        let n = 10_000;
308        let inter_arrivals: Vec<f64> = (0..n).map(|_| pp.next_arrival(&mut rng)).collect();
309        let mean = inter_arrivals.iter().sum::<f64>() / n as f64;
310        let expected = 1.0 / rate;
311
312        assert!(
313            (mean - expected).abs() < 0.05,
314            "inter-arrival mean {} should be ~{}",
315            mean,
316            expected
317        );
318    }
319
320    #[test]
321    fn test_poisson_arrivals_sorted() {
322        let pp = PoissonProcess::new(2.0);
323        let mut rng = Rng::new(42);
324        let arrivals = pp.arrivals(&mut rng, 100.0);
325        for w in arrivals.windows(2) {
326            assert!(w[0] < w[1], "arrivals should be sorted");
327        }
328    }
329
330    #[test]
331    fn test_poisson_distribution_pmf() {
332        let pd = PoissonDistribution::new(3.0);
333        // P(0) = e^{-3} ≈ 0.0498
334        assert!((pd.pmf(0) - (-3.0_f64).exp()).abs() < 1e-10);
335        // Sum of all probabilities should be ~1
336        let total: f64 = (0..30).map(|k| pd.pmf(k)).sum();
337        assert!((total - 1.0).abs() < 1e-6);
338    }
339
340    #[test]
341    fn test_poisson_distribution_sample_mean() {
342        let pd = PoissonDistribution::new(5.0);
343        let mut rng = Rng::new(42);
344        let n = 10_000;
345        let mean: f64 = (0..n).map(|_| pd.sample(&mut rng) as f64).sum::<f64>() / n as f64;
346        assert!(
347            (mean - 5.0).abs() < 0.3,
348            "sample mean {} should be ~5",
349            mean
350        );
351    }
352
353    #[test]
354    fn test_nhpp_thinning() {
355        // Sinusoidal rate: lambda(t) = 5 + 3*sin(t), max = 8
356        let nhpp = NonHomogeneousPoisson::new(
357            Box::new(|t: f64| 5.0 + 3.0 * t.sin()),
358            8.0,
359        );
360        let mut rng = Rng::new(42);
361        let arrivals = nhpp.arrivals(&mut rng, 10.0);
362        // Should have some events
363        assert!(!arrivals.is_empty());
364        // All within duration
365        assert!(arrivals.iter().all(|&t| t >= 0.0 && t <= 10.0));
366    }
367
368    #[test]
369    fn test_compound_poisson() {
370        let cp = CompoundPoisson::normal_jumps(2.0, 1.0, 0.5);
371        let mut rng = Rng::new(42);
372        let path = cp.path(&mut rng, 10.0);
373        assert!(path.len() >= 2); // at least start and end
374        assert_eq!(path[0], (0.0, 0.0));
375    }
376
377    #[test]
378    fn test_superpose_and_thin() {
379        let p1 = PoissonProcess::new(3.0);
380        let p2 = PoissonProcess::new(5.0);
381        let merged = p1.superpose(&p2);
382        assert!((merged.rate - 8.0).abs() < 1e-10);
383
384        let thinned = merged.thin(0.5);
385        assert!((thinned.rate - 4.0).abs() < 1e-10);
386    }
387
388    #[test]
389    fn test_counting_path() {
390        let pp = PoissonProcess::new(2.0);
391        let mut rng = Rng::new(42);
392        let path = pp.counting_path(&mut rng, 5.0);
393        assert_eq!(path[0], (0.0, 0));
394        // Count should be monotonically non-decreasing
395        for w in path.windows(2) {
396            assert!(w[1].1 >= w[0].1);
397        }
398    }
399}