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            if s.growth_rate > 0.0 {
87                // Logistic growth: dN/dt = rN(1 - N/K)
88                dpop[i] += s.growth_rate * s.population * (1.0 - s.population / s.carrying_capacity.max(1.0));
89            } else {
90                // A negative rate is a death rate (predators without prey):
91                // dN/dt = rN. Putting it through the logistic term flipped
92                // its sign above K, so a large predator population grew
93                // without food and wiped out its prey.
94                dpop[i] += s.growth_rate * s.population;
95            }
96        }
97
98        // Interaction effects
99        for inter in &self.interactions {
100            let pred_idx = self.species.iter().position(|s| s.id == inter.predator);
101            let prey_idx = self.species.iter().position(|s| s.id == inter.prey);
102            if let (Some(pi), Some(qi)) = (pred_idx, prey_idx) {
103                let pred_pop = self.species[pi].population;
104                let prey_pop = self.species[qi].population;
105                let alpha = inter.strength;
106
107                match inter.interaction_type {
108                    InteractionType::Predation => {
109                        dpop[pi] += alpha * pred_pop * prey_pop * 0.01;  // predator gains
110                        dpop[qi] -= alpha * pred_pop * prey_pop * 0.01;  // prey loses
111                    }
112                    InteractionType::Competition => {
113                        dpop[pi] -= alpha * pred_pop * prey_pop * 0.005;
114                        dpop[qi] -= alpha * pred_pop * prey_pop * 0.005;
115                    }
116                    InteractionType::Mutualism => {
117                        dpop[pi] += alpha * prey_pop * 0.001;
118                        dpop[qi] += alpha * pred_pop * 0.001;
119                    }
120                    InteractionType::Parasitism => {
121                        dpop[pi] += alpha * prey_pop * 0.002;
122                        dpop[qi] -= alpha * pred_pop * 0.003;
123                    }
124                    InteractionType::Commensalism => {
125                        dpop[pi] += alpha * prey_pop * 0.001;
126                    }
127                }
128            }
129        }
130
131        // Apply
132        for i in 0..n {
133            self.species[i].population = (self.species[i].population + dpop[i] * dt).max(0.0);
134        }
135
136        self.time += dt;
137
138        // Record history every ~1 time unit
139        if self.history.is_empty() || self.time - self.history.last().unwrap().0 >= 1.0 {
140            let pops: Vec<f64> = self.species.iter().map(|s| s.population).collect();
141            self.history.push((self.time, pops));
142        }
143    }
144
145    /// Run for a number of steps.
146    pub fn simulate(&mut self, steps: usize, dt: f64) {
147        for _ in 0..steps {
148            self.step(dt);
149        }
150    }
151
152    /// Lyapunov stability analysis: compute largest Lyapunov exponent.
153    pub fn lyapunov_exponent(&self, dt: f64, steps: usize) -> f64 {
154        let mut eco = self.clone();
155        let mut perturbed = self.clone();
156        // Small perturbation
157        if !perturbed.species.is_empty() {
158            perturbed.species[0].population *= 1.0001;
159        }
160
161        let mut sum_log = 0.0_f64;
162        let d0 = (eco.species[0].population - perturbed.species[0].population).abs().max(1e-15);
163
164        for i in 0..steps {
165            eco.step(dt);
166            perturbed.step(dt);
167
168            let d = (eco.species[0].population - perturbed.species[0].population).abs().max(1e-15);
169            sum_log += (d / d0).ln();
170
171            // Renormalize perturbation
172            if !perturbed.species.is_empty() {
173                let scale = d0 / d;
174                for j in 0..perturbed.species.len() {
175                    let diff = perturbed.species[j].population - eco.species[j].population;
176                    perturbed.species[j].population = eco.species[j].population + diff * scale;
177                }
178            }
179        }
180
181        sum_log / (steps as f64 * dt)
182    }
183
184    /// Total biomass.
185    pub fn total_biomass(&self) -> f64 {
186        self.species.iter().map(|s| s.population * s.traits.size).sum()
187    }
188
189    /// Species diversity (Shannon index).
190    pub fn shannon_diversity(&self) -> f64 {
191        let total: f64 = self.species.iter().map(|s| s.population).sum();
192        if total < 1.0 { return 0.0; }
193        -self.species.iter()
194            .map(|s| {
195                let p = s.population / total;
196                if p > 0.0 { p * p.ln() } else { 0.0 }
197            })
198            .sum::<f64>()
199    }
200}
201
202/// Create a classic predator-prey ecosystem.
203pub fn lotka_volterra_example() -> Ecosystem {
204    let mut eco = Ecosystem::new();
205    eco.add_species(Species {
206        id: 0, name: "Rabbit".to_string(), population: 100.0,
207        carrying_capacity: 500.0, growth_rate: 0.5,
208        trophic_level: TrophicLevel::PrimaryConsumer,
209        traits: SpeciesTraits { size: 0.1, speed: 0.6, reproduction_rate: 0.8, lifespan: 5.0, aggression: 0.1 },
210    });
211    eco.add_species(Species {
212        id: 1, name: "Fox".to_string(), population: 20.0,
213        carrying_capacity: 100.0, growth_rate: -0.1,
214        trophic_level: TrophicLevel::SecondaryConsumer,
215        traits: SpeciesTraits { size: 0.3, speed: 0.8, reproduction_rate: 0.3, lifespan: 10.0, aggression: 0.7 },
216    });
217    eco.add_interaction(Interaction {
218        predator: 1, prey: 0,
219        interaction_type: InteractionType::Predation,
220        strength: 0.5,
221    });
222    eco
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_lotka_volterra() {
231        let mut eco = lotka_volterra_example();
232        eco.simulate(1000, 0.1);
233        // Both species should survive
234        assert!(eco.species[0].population > 0.0, "rabbits should survive");
235        assert!(eco.species[1].population > 0.0, "foxes should survive");
236    }
237
238    #[test]
239    fn test_shannon_diversity() {
240        let eco = lotka_volterra_example();
241        let h = eco.shannon_diversity();
242        assert!(h > 0.0, "diversity should be positive with 2 species");
243    }
244
245    #[test]
246    fn test_history_recording() {
247        let mut eco = lotka_volterra_example();
248        eco.simulate(100, 0.1);
249        assert!(!eco.history.is_empty());
250    }
251}