proof_engine/ecology/
population.rs1pub fn logistic_growth(pop: f64, rate: f64, capacity: f64, dt: f64) -> f64 {
5 (pop + rate * pop * (1.0 - pop / capacity.max(1.0)) * dt).max(0.0)
6}
7
8pub fn lotka_volterra_step(
11 prey: f64, predator: f64,
12 prey_growth: f64, predation_rate: f64,
13 predator_death: f64, conversion_rate: f64,
14 dt: f64,
15) -> (f64, f64) {
16 let dprey = (prey_growth * prey - predation_rate * prey * predator) * dt;
17 let dpred = (conversion_rate * prey * predator - predator_death * predator) * dt;
18 ((prey + dprey).max(0.0), (predator + dpred).max(0.0))
19}
20
21pub fn competition_step(
24 n1: f64, n2: f64,
25 r1: f64, r2: f64,
26 k1: f64, k2: f64,
27 alpha12: f64, alpha21: f64,
28 dt: f64,
29) -> (f64, f64) {
30 let dn1 = r1 * n1 * (1.0 - (n1 + alpha12 * n2) / k1) * dt;
31 let dn2 = r2 * n2 * (1.0 - (n2 + alpha21 * n1) / k2) * dt;
32 ((n1 + dn1).max(0.0), (n2 + dn2).max(0.0))
33}
34
35pub fn allee_growth(pop: f64, rate: f64, capacity: f64, allee_threshold: f64, dt: f64) -> f64 {
37 let growth = rate * pop * (pop / allee_threshold - 1.0) * (1.0 - pop / capacity);
38 (pop + growth * dt).max(0.0)
39}
40
41pub fn beverton_holt(pop: f64, r: f64, k: f64) -> f64 {
43 r * pop / (1.0 + (r - 1.0) * pop / k)
44}
45
46pub fn ricker(pop: f64, r: f64, k: f64) -> f64 {
48 pop * (r * (1.0 - pop / k)).exp()
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn test_logistic_approaches_capacity() {
57 let mut pop = 10.0;
58 for _ in 0..1000 {
59 pop = logistic_growth(pop, 0.5, 100.0, 0.1);
60 }
61 assert!((pop - 100.0).abs() < 1.0, "should approach K=100: got {pop}");
62 }
63
64 #[test]
65 fn test_lotka_volterra_oscillation() {
66 let (mut prey, mut pred) = (100.0, 20.0);
67 let mut max_prey = 0.0_f64;
68 let mut min_prey = f64::MAX;
69 for _ in 0..10000 {
70 let (np, nd) = lotka_volterra_step(prey, pred, 0.5, 0.01, 0.3, 0.005, 0.01);
71 prey = np; pred = nd;
72 max_prey = max_prey.max(prey);
73 min_prey = min_prey.min(prey);
74 }
75 assert!(max_prey > min_prey * 1.5, "should oscillate");
76 }
77
78 #[test]
79 fn test_competition_coexistence() {
80 let (mut n1, mut n2) = (50.0, 50.0);
81 for _ in 0..10000 {
82 let (a, b) = competition_step(n1, n2, 0.3, 0.3, 200.0, 200.0, 0.5, 0.5, 0.1);
83 n1 = a; n2 = b;
84 }
85 assert!(n1 > 1.0 && n2 > 1.0, "coexistence with weak competition");
86 }
87
88 #[test]
89 fn test_ricker_bounded() {
90 let mut pop = 10.0;
91 for _ in 0..100 {
92 pop = ricker(pop, 2.0, 100.0);
93 assert!(pop >= 0.0 && pop < 1000.0, "Ricker should stay bounded: {pop}");
94 }
95 }
96}