Skip to main content

proof_engine/ecology/
food_web.rs

1//! Food web simulation — directed graph of trophic relationships.
2
3use std::collections::{HashMap, HashSet};
4
5/// A node in the food web.
6#[derive(Debug, Clone)]
7pub struct WebNode {
8    pub species_id: u32,
9    pub name: String,
10    pub trophic_level: f32,
11    pub biomass: f64,
12}
13
14/// An edge in the food web (energy flow from prey to predator).
15#[derive(Debug, Clone)]
16pub struct WebEdge {
17    pub prey: u32,
18    pub predator: u32,
19    pub transfer_efficiency: f64,
20}
21
22/// The food web graph.
23#[derive(Debug, Clone)]
24pub struct FoodWeb {
25    pub nodes: Vec<WebNode>,
26    pub edges: Vec<WebEdge>,
27}
28
29impl FoodWeb {
30    pub fn new() -> Self { Self { nodes: Vec::new(), edges: Vec::new() } }
31
32    pub fn add_node(&mut self, node: WebNode) { self.nodes.push(node); }
33    pub fn add_edge(&mut self, edge: WebEdge) { self.edges.push(edge); }
34
35    /// Connectance: fraction of possible links that exist.
36    pub fn connectance(&self) -> f64 {
37        let n = self.nodes.len() as f64;
38        if n < 2.0 { return 0.0; }
39        self.edges.len() as f64 / (n * (n - 1.0))
40    }
41
42    /// Find all prey of a predator.
43    pub fn prey_of(&self, predator_id: u32) -> Vec<u32> {
44        self.edges.iter().filter(|e| e.predator == predator_id).map(|e| e.prey).collect()
45    }
46
47    /// Find all predators of a prey.
48    pub fn predators_of(&self, prey_id: u32) -> Vec<u32> {
49        self.edges.iter().filter(|e| e.prey == prey_id).map(|e| e.predator).collect()
50    }
51
52    /// Compute trophic levels using shortest path from producers.
53    pub fn compute_trophic_levels(&mut self) {
54        let producers: Vec<u32> = self.nodes.iter()
55            .filter(|n| self.prey_of(n.species_id).is_empty())
56            .map(|n| n.species_id)
57            .collect();
58
59        for node in &mut self.nodes {
60            if producers.contains(&node.species_id) {
61                node.trophic_level = 1.0;
62            }
63        }
64
65        // BFS-like propagation
66        for _ in 0..10 {
67            for i in 0..self.nodes.len() {
68                let id = self.nodes[i].species_id;
69                let prey_levels: Vec<f32> = self.prey_of(id).iter()
70                    .filter_map(|&pid| self.nodes.iter().find(|n| n.species_id == pid))
71                    .map(|n| n.trophic_level)
72                    .collect();
73                if !prey_levels.is_empty() {
74                    let avg: f32 = prey_levels.iter().sum::<f32>() / prey_levels.len() as f32;
75                    self.nodes[i].trophic_level = avg + 1.0;
76                }
77            }
78        }
79    }
80
81    /// Total energy flow through the web.
82    pub fn total_energy_flow(&self) -> f64 {
83        self.edges.iter().map(|e| {
84            let prey_biomass = self.nodes.iter()
85                .find(|n| n.species_id == e.prey)
86                .map(|n| n.biomass)
87                .unwrap_or(0.0);
88            prey_biomass * e.transfer_efficiency
89        }).sum()
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_food_web() {
99        let mut web = FoodWeb::new();
100        web.add_node(WebNode { species_id: 0, name: "Grass".into(), trophic_level: 1.0, biomass: 1000.0 });
101        web.add_node(WebNode { species_id: 1, name: "Rabbit".into(), trophic_level: 2.0, biomass: 100.0 });
102        web.add_node(WebNode { species_id: 2, name: "Fox".into(), trophic_level: 3.0, biomass: 10.0 });
103        web.add_edge(WebEdge { prey: 0, predator: 1, transfer_efficiency: 0.1 });
104        web.add_edge(WebEdge { prey: 1, predator: 2, transfer_efficiency: 0.1 });
105
106        assert_eq!(web.prey_of(1), vec![0]);
107        assert_eq!(web.predators_of(1), vec![2]);
108        assert!(web.connectance() > 0.0);
109    }
110}