Skip to content

Commit 4c2425e

Browse files
committed
mean field base model with tests done
1 parent a36a8c7 commit 4c2425e

5 files changed

Lines changed: 178 additions & 29 deletions

File tree

models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .mean_field import MeanFieldModel

models/mean_field.py

Lines changed: 133 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,76 @@
11
import numpy as np
2-
from scipy.integrate import odeint
3-
from scipy.ndimage import label
4-
from scipy.optimize import curve_fit
5-
from scipy.stats import kstest
62
import matplotlib.pyplot as plt
73
from typing import Tuple, List, Dict, Optional
4+
from scipy.integrate import solve_ivp
5+
from scipy.integrate import odeint
86

97
class MeanFieldModel:
108
"""
119
Mean-field (non-spatial) predator-prey model.
10+
11+
Equations:
12+
dR/dt = R * (b - d_r - c*C - e*R)
13+
dC/dt = C * (a*R - d_c - q*C)
14+
15+
where:
16+
R: Prey population density
17+
C: Predator population density
18+
b: Prey birth rate
19+
d_r: Prey death rate
20+
c: Consumption rate of prey by predators
21+
e: Intraspecific competition among prey
22+
a: Conversion efficiency of prey into predator offspring
23+
d_c: Predator death rate
24+
q: Intraspecific competition among predators
1225
"""
1326

14-
def __init__(self, birth: float, consumption: float, predator_death: float):
27+
def __init__(self,
28+
birth: float = 0.2,
29+
consumption: float = 0.8,
30+
predator_death: float = 0.045,
31+
conversion: float = 1.0,
32+
prey_competition: float = 0.1,
33+
predator_competition:float = 0.05):
1534
"""
1635
Initialize the mean-field model with given parameters.
1736
Args:
1837
birth (float): Prey birth rate (b)
19-
consumption (float): predator consumption rate (c)
20-
predator_death (float): predator death rat (d_c)
38+
consumption (float): Consumption rate of prey by predators (c)
39+
predator_death (float): Predator death rate (d_c)
40+
conversion (float): Conversion efficiency of prey into predator offspring (a)
41+
prey_competition (float): Intraspecific competition among prey (e)
42+
predator_competition (float): Intraspecific competition among predators (q)
2143
"""
44+
self.birth = birth
45+
self.consumption = consumption
46+
self.predator_death = predator_death
47+
self.conversion = conversion
48+
self.pred_benifit = self.consumption * self.conversion
49+
self.prey_competition = prey_competition
50+
self.predator_competition = predator_competition
2251

23-
def ode_system(self, Z: np.ndarray, t: float, prey_death: float)->List[float]:
52+
def ode_system(self, Z: np.ndarray, t: float, prey_death: float)->list:
2453
"""
25-
Mean-field ODE system for predator prey dynamics
26-
27-
Args:
28-
Z (np.ndarray): _description_
29-
t (float): _description_
30-
prey_death (float): _description_
31-
32-
Returns:
33-
List[float]: _description_
54+
Mean-field ODE system for predator prey dynamics.
3455
"""
35-
pass
56+
R, C = Z
57+
58+
R = max(R, 0)
59+
C = max(C,0)
60+
61+
# Net prey growth rate
62+
r = self.birth - prey_death
63+
64+
# Prey dynamics: growth - predation - competition
65+
dR = R * (r - self.consumption * C - self.prey_competition * R)
66+
67+
# Predator dynamics: growth from predation - death - competition
68+
dC = C * (self.conversion * self.consumption * R - self.predator_death - self.predator_competition * C)
69+
70+
return [dR, dC]
71+
3672

37-
def solve(self, prey_death: float, Z0: Tuple[float, float], t_max: float, n_points: int)->Tuple[np.ndarray, np.ndarray]:
73+
def solve(self, prey_death: float = 0.5, R0: float = 0.5, Z0: float = 0.2, t_max: float = 500, n_points: int = 1000)->Tuple[np.ndarray, np.ndarray]:
3874
"""
3975
Solve the mean-field ODE system.
4076
@@ -47,28 +83,97 @@ def solve(self, prey_death: float, Z0: Tuple[float, float], t_max: float, n_poin
4783
Returns:
4884
Tuple[np.ndarray, np.ndarray]: Time points and solution array
4985
"""
50-
pass
86+
t = np.linspace(0, t_max, n_points)
87+
Z0 = [R0, Z0]
88+
89+
sol = odeint(self.ode_system, Z0, t, args=(prey_death,))
90+
91+
return t, sol
5192

5293
def equilibrium(self, prey_death: float)->Tuple[float, float]:
5394
"""
54-
Calculate the equilibrium point of the system.
95+
Calculate the equilibrium densities of the system.
5596
5697
Args:
5798
prey_death (float): Prey death rate (d)
5899
59100
Returns:
60101
Tuple[float, float]: Equilibrium populations (prey, predator)
61102
"""
62-
pass
103+
r = self.birth - prey_death
104+
c = self.consumption
105+
a = self.pred_benifit
106+
e = self.prey_competition
107+
q = self.predator_competition
108+
d_c = self.predator_death
109+
110+
if r <= 0:
111+
return (0.0, 0.0)
112+
113+
R_prey = r / e
114+
115+
# Check if predator can invade
116+
predator_invasion_fitness = a * R_prey - d_c
117+
if predator_invasion_fitness <= 0:
118+
return (R_prey, 0.0) # Predator cannot persist
119+
120+
# Coexistence equilibrium
121+
R_n = r * q + d_c * c
122+
R_d = c * a + e * q
123+
124+
if R_d <= 0:
125+
return (R_prey, 0.0)
126+
127+
R_star = R_n / R_d
128+
C_star = (a * R_star - d_c) / q
129+
130+
if R_star < 0 or C_star < 0:
131+
if r > 0:
132+
return (R_prey, 0.0)
133+
else:
134+
return (0.0, 0.0)
135+
136+
return (R_star, C_star)
63137

64-
def sweep_death_rate(self, d_r_values: np.ndarray, t_equilibrium: float) -> Dict[str, np.ndarray]:
138+
def equilibrium_numerical(self, prey_death: float, t_max: float = 1000)->Tuple[float, float]:
65139
"""
66-
Sweep prey death rate and record equilibrium densities.
140+
Find equilibrium densities numerically by solving ODEs over a long time.
67141
"""
68-
pass
142+
t, Z = self.solve(prey_death=prey_death, t_max=t_max)
143+
R_eq = max(0, np.mean(Z[-100:, 0]))
144+
C_eq = max(0, np.mean(Z[-100:, 1]))
145+
return (R_eq, C_eq)
69146

70-
def nullclines(self, prey_death: float, Z_r_range: np.ndarray)->Dict[str, np.ndarray]:
147+
def sweep_death_rate(self, d_r_values: np.ndarray, method: str = 'analytical') -> Dict[str, np.ndarray]:
71148
"""
72-
Compute nullclines for phase portrait.
149+
Sweep prey death rate and record equilibrium densities.
73150
"""
74-
pass
151+
n = len(d_r_values)
152+
R_eq = np.zeros(n)
153+
C_eq = np.zeros(n)
154+
155+
for i, d_r in enumerate(d_r_values):
156+
if method == 'analytical':
157+
R_eq[i], C_eq[i] = self.equilibrium(d_r)
158+
else:
159+
R_eq[i], C_eq[i] = self.equilibrium_numerical(d_r)
160+
161+
return {'d_r': d_r_values, 'R_eq': R_eq, 'C_eq': C_eq, 'net_growth': self.birth - d_r_values}
162+
163+
164+
165+
# FIXME: Add plotting utilities
166+
167+
168+
if __name__== '__main__':
169+
print("Mean-Field Model Module")
170+
mf = MeanFieldModel()
171+
172+
print('Model Parameters:')
173+
print(f'Birth rate: {mf.birth}')
174+
print(f'Consumption rate: {mf.consumption}')
175+
print(f'Predator death rate: {mf.predator_death}')
176+
print(f'Conversion efficiency: {mf.conversion}')
177+
print(f'Prey competition: {mf.prey_competition}')
178+
print(f'Predator competition: {mf.predator_competition}')
179+

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
matplotlib
22
numpy
3-
scipy
3+
scipy
4+
pytest

tests/__init__.py

Whitespace-only changes.

tests/test_mf.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
import numpy as np
3+
from models.mean_field import MeanFieldModel
4+
5+
@pytest.fixture
6+
def model():
7+
"""Model instance for testing."""
8+
return MeanFieldModel()
9+
10+
11+
def test_initialization(model):
12+
"""Test model initialization with default parameters."""
13+
assert model.birth == 0.2
14+
assert model.consumption == 0.8
15+
assert model.predator_death == 0.045
16+
assert model.conversion == 1.0
17+
assert model.prey_competition == 0.1
18+
assert model.predator_competition == 0.05
19+
assert model.pred_benifit == model.consumption * model.conversion
20+
21+
22+
def test_prey_extinction(model):
23+
"""Verify prey extinction logic."""
24+
R_eq, C_eq = model.equilibrium(prey_death=0.3)
25+
assert R_eq == 0.0
26+
assert C_eq == 0.0
27+
28+
def test_monotonicity(model):
29+
"""Test monotonicity of equilibrium populations with respect to prey death rate."""
30+
d_r_range = np.linspace(0.01, 0.08, 10)
31+
sweep = model.sweep_death_rate(d_r_range)
32+
assert np.all(np.diff(sweep['R_eq']) <= 0)
33+
34+
35+
def test_convergence(model):
36+
ana_R, _ = model.equilibrium(0.05)
37+
num_R, _ = model.equilibrium_numerical(0.05)
38+
39+
# Use approx for floating point comparisons in numerical analysis
40+
assert num_R == pytest.approx(ana_R, rel=1e-2)
41+
42+

0 commit comments

Comments
 (0)