Skip to main content

proof_engine/ecology/
mod.rs

1//! Mathematical ecology simulation.
2//!
3//! Lotka-Volterra predator-prey, logistic growth, competition models,
4//! food webs, migration, evolution, stability analysis, disease, symbiosis.
5//! All dynamics are rendered in real time.
6
7pub mod population;
8pub mod food_web;
9pub mod migration;
10pub mod evolution;
11pub mod disease;
12
13use std::collections::HashMap;
14
15/// A species in the ecosystem.
16#[derive(Debug, Clone)]
17pub struct Species {
18    pub id: u32,
19    pub name: String,
20    pub population: f64,
21    pub carrying_capacity: f64,
22    pub growth_rate: f64,
23    pub trophic_level: TrophicLevel,
24    pub traits: SpeciesTraits,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum TrophicLevel { Producer, PrimaryConsumer, SecondaryConsumer, ApexPredator, Decomposer }
29
30#[derive(Debug, Clone, Copy)]
31pub struct SpeciesTraits {
32    pub size: f64,
33    pub speed: f64,
34    pub reproduction_rate: f64,
35    pub lifespan: f64,
36    pub aggression: f64,
37}
38
39/// An ecosystem containing multiple interacting species.
40#[derive(Debug, Clone)]
41pub struct Ecosystem {
42    pub species: Vec<Species>,
43    pub interactions: Vec<Interaction>,
44    pub time: f64,
45    pub history: Vec<(f64, Vec<f64>)>,
46}
47
48/// Interaction between two species.
49#[derive(Debug, Clone)]
50pub struct Interaction {
51    pub predator: u32,
52    pub prey: u32,
53    pub interaction_type: InteractionType,
54    pub strength: f64,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum InteractionType {
59    Predation,     // +/- (predator gains, prey loses)
60    Competition,   // -/- (both lose)
61    Mutualism,     // +/+ (both gain)
62    Parasitism,    // +/- (parasite gains, host loses)
63    Commensalism,  // +/0 (one gains, other unaffected)
64}
65
66impl Ecosystem {
67    pub fn new() -> Self {
68        Self { species: Vec::new(), interactions: Vec::new(), time: 0.0, history: Vec::new() }
69    }
70
71    pub fn add_species(&mut self, species: Species) {
72        self.species.push(species);
73    }
74
75    pub fn add_interaction(&mut self, interaction: Interaction) {
76        self.interactions.push(interaction);
77    }
78
79    /// Step the ecosystem by dt using Lotka-Volterra equations.
80    pub fn step(&mut self, dt: f64) {
81        let n = self.species.len();
82        let mut dpop = vec![0.0f64; n];
83
84        for i in 0..n {
85            let s = &self.species[i];
86            // Logistic growth: dN/dt = rN(1 - N/K)
87            dpop[i] += s.growth_rate * s.population * (1.0 - s.population / s.carrying_capacity.max(1.0));
88        }
89
90        // Interaction effects
91        for inter in &self.interactions {
92            let pred_idx = self.species.iter().position(|s| s.id == inter.predator);
93            let prey_idx = self.species.iter().position(|s| s.id == inter.prey);
94            if let (Some(pi), Some(qi)) = (pred_idx, prey_idx) {
95                let pred_pop = self.species[pi].population;
96                let prey_pop = self.species[qi].population;
97                let alpha = inter.strength;
98
99                match inter.interaction_type {
100                    InteractionType::Predation => {
101                        dpop[pi] += alpha * pred_pop * prey_pop * 0.01;  // predator gains
102                        dpop[qi] -= alpha * pred_pop * prey_pop * 0.01;  // prey loses
103                    }
104                    InteractionType::Competition => {
105                        dpop[pi] -= alpha * pred_pop * prey_pop * 0.005;
106                        dpop[qi] -= alpha * pred_pop * prey_pop * 0.005;
107                    }
108                    InteractionType::Mutualism => {
109                        dpop[pi] += alpha * prey_pop * 0.001;
110                        dpop[qi] += alpha * pred_pop * 0.001;
111                    }
112                    InteractionType::Parasitism => {
113                        dpop[pi] += alpha * prey_pop * 0.002;
114                        dpop[qi] -= alpha * pred_pop * 0.003;
115                    }
116                    InteractionType::Commensalism => {
117                        dpop[pi] += alpha * prey_pop * 0.001;
118                    }
119                }
120            }
121        }
122
123        // Apply
124        for i in 0..n {
125            self.species[i].population = (self.species[i].population + dpop[i] * dt).max(0.0);
126        }
127
128        self.time += dt;
129
130        // Record history every ~1 time unit
131        if self.history.is_empty() || self.time - self.history.last().unwrap().0 >= 1.0 {
132            let pops: Vec<f64> = self.species.iter().map(|s| s.population).collect();
133            self.history.push((self.time, pops));
134        }
135    }
136
137    /// Run for a number of steps.
138    pub fn simulate(&mut self, steps: usize, dt: f64) {
139        for _ in 0..steps {
140            self.step(dt);
141        }
142    }
143
144    /// Lyapunov stability analysis: compute largest Lyapunov exponent.
145    pub fn lyapunov_exponent(&self, dt: f64, steps: usize) -> f64 {
146        let mut eco = self.clone();
147        let mut perturbed = self.clone();
148        // Small perturbation
149        if !perturbed.species.is_empty() {
150            perturbed.species[0].population *= 1.0001;
151        }
152
153        let mut sum_log = 0.0_f64;
154        let d0 = (eco.species[0].population - perturbed.species[0].population).abs().max(1e-15);
155
156        for i in 0..steps {
157            eco.step(dt);
158            perturbed.step(dt);
159
160            let d = (eco.species[0].population - perturbed.species[0].population).abs().max(1e-15);
161            sum_log += (d / d0).ln();
162
163            // Renormalize perturbation
164            if !perturbed.species.is_empty() {
165                let scale = d0 / d;
166                for j in 0..perturbed.species.len() {
167                    let diff = perturbed.species[j].population - eco.species[j].population;
168                    perturbed.species[j].population = eco.species[j].population + diff * scale;
169                }
170            }
171        }
172
173        sum_log / (steps as f64 * dt)
174    }
175
176    /// Total biomass.
177    pub fn total_biomass(&self) -> f64 {
178        self.species.iter().map(|s| s.population * s.traits.size).sum()
179    }
180
181    /// Species diversity (Shannon index).
182    pub fn shannon_diversity(&self) -> f64 {
183        let total: f64 = self.species.iter().map(|s| s.population).sum();
184        if total < 1.0 { return 0.0; }
185        -self.species.iter()
186            .map(|s| {
187                let p = s.population / total;
188                if p > 0.0 { p * p.ln() } else { 0.0 }
189            })
190            .sum::<f64>()
191    }
192}
193
194/// Create a classic predator-prey ecosystem.
195pub fn lotka_volterra_example() -> Ecosystem {
196    let mut eco = Ecosystem::new();
197    eco.add_species(Species {
198        id: 0, name: "Rabbit".to_string(), population: 100.0,
199        carrying_capacity: 500.0, growth_rate: 0.5,
200        trophic_level: TrophicLevel::PrimaryConsumer,
201        traits: SpeciesTraits { size: 0.1, speed: 0.6, reproduction_rate: 0.8, lifespan: 5.0, aggression: 0.1 },
202    });
203    eco.add_species(Species {
204        id: 1, name: "Fox".to_string(), population: 20.0,
205        carrying_capacity: 100.0, growth_rate: -0.1,
206        trophic_level: TrophicLevel::SecondaryConsumer,
207        traits: SpeciesTraits { size: 0.3, speed: 0.8, reproduction_rate: 0.3, lifespan: 10.0, aggression: 0.7 },
208    });
209    eco.add_interaction(Interaction {
210        predator: 1, prey: 0,
211        interaction_type: InteractionType::Predation,
212        strength: 0.5,
213    });
214    eco
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_lotka_volterra() {
223        let mut eco = lotka_volterra_example();
224        eco.simulate(1000, 0.1);
225        // Both species should survive
226        assert!(eco.species[0].population > 0.0, "rabbits should survive");
227        assert!(eco.species[1].population > 0.0, "foxes should survive");
228    }
229
230    #[test]
231    fn test_shannon_diversity() {
232        let eco = lotka_volterra_example();
233        let h = eco.shannon_diversity();
234        assert!(h > 0.0, "diversity should be positive with 2 species");
235    }
236
237    #[test]
238    fn test_history_recording() {
239        let mut eco = lotka_volterra_example();
240        eco.simulate(100, 0.1);
241        assert!(!eco.history.is_empty());
242    }
243}