Skip to content

Commit fe48085

Browse files
committed
Create predator-prey class
All that needs to be done is add the evolution of death rates to get the MVP
1 parent c93299c commit fe48085

3 files changed

Lines changed: 264 additions & 1 deletion

File tree

CA.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,107 @@ def run(self, steps: int) -> None:
134134
for _ in range(steps):
135135
self.update()
136136

137+
138+
class PP(CA):
139+
"""Predator-Prey cellular automaton.
140+
141+
States: 0 = empty, 1 = prey, 2 = predator
142+
143+
Expected params keys (all values in [0,1]):
144+
- "prey_death": probability a prey dies each step
145+
- "predator_death": probability a predator dies each step
146+
- "reproduction": per-neighbor prey reproduction probability
147+
- "consumption": per-neighbor predation probability
148+
149+
Defaults are provided for any missing keys and user-provided values
150+
are preserved. Any unknown keys in `params` will raise a ValueError.
151+
"""
152+
153+
def __init__(
154+
self,
155+
rows: int,
156+
cols: int,
157+
densities: Tuple[float, ...],
158+
neighborhood: str,
159+
params: Dict[str, object],
160+
cell_params: Dict[str, object],
161+
seed: Optional[int] = None,
162+
) -> None:
163+
# initialize base CA
164+
super().__init__(rows, cols, densities, neighborhood, params, cell_params, seed)
165+
166+
# Enforce predator-prey has exactly two species
167+
assert self.n_species == 2, "PP model requires exactly two species (prey=1, predator=2)"
168+
169+
# Allowed parameter keys and defaults
170+
_allowed = {
171+
"prey_death": 0.02,
172+
"predator_death": 0.05,
173+
"reproduction": 0.2,
174+
"consumption": 0.5,
175+
}
176+
177+
# Check for unknown user-specified keys (in the params dict provided by user)
178+
user_keys = set(self.params.keys())
179+
unknown = user_keys.difference(_allowed.keys())
180+
if len(unknown) > 0:
181+
raise ValueError(f"Unknown parameter keys for PP: {sorted(list(unknown))}")
182+
183+
# Fill defaults for missing keys without overriding user-specified values
184+
for k, v in _allowed.items():
185+
if k not in self.params:
186+
self.params[k] = v
187+
188+
# Validate parameter ranges
189+
for k in _allowed.keys():
190+
val = self.params[k]
191+
if not isinstance(val, (int, float)):
192+
raise TypeError(f"Parameter '{k}' must be numeric")
193+
if not (0.0 <= float(val) <= 1.0):
194+
raise ValueError(f"Parameter '{k}' must be between 0 and 1 (got {val})")
195+
196+
def update(self) -> None:
197+
"""One update step for predator-prey dynamics.
198+
199+
Uses a copy of the current grid to evaluate rules so newly changed
200+
cells do not immediately influence other rules in the same step.
201+
"""
202+
# copy of the grid to base all decisions on
203+
old = self.grid.copy()
204+
205+
# neighbor counts for each species (index 0 -> prey, 1 -> predator)
206+
counts = self.count_neighbors()
207+
prey_neighbors = counts[0]
208+
pred_neighbors = counts[1] if self.n_species >= 2 else np.zeros_like(self.grid)
209+
210+
rows, cols = self.grid.shape
211+
# Reproduction into empty cells from neighboring prey
212+
empty_mask = old == 0
213+
# probability that at least one neighboring prey reproduces into the cell
214+
birth_param = float(self.params["reproduction"])
215+
birth_prob = 1.0 - np.power(1.0 - birth_param, prey_neighbors)
216+
rand = self.generator.random(size=(rows, cols))
217+
birth_cells = empty_mask & (prey_neighbors > 0) & (rand < birth_prob)
218+
self.grid[birth_cells] = 1
219+
220+
# Predation: prey replaced by predator due to neighboring predators
221+
prey_mask = old == 1
222+
cons_param = float(self.params["consumption"])
223+
cons_prob = 1.0 - np.power(1.0 - cons_param, pred_neighbors)
224+
rand = self.generator.random(size=(rows, cols))
225+
predation_cells = prey_mask & (pred_neighbors > 0) & (rand < cons_prob)
226+
self.grid[predation_cells] = 2
227+
228+
# Deaths: use the copied `old` grid so newly-occupied cells are not killed immediately
229+
# Prey death
230+
prey_death_p = float(self.params["prey_death"])
231+
rand = self.generator.random(size=(rows, cols))
232+
prey_death_cells = (old == 1) & (rand < prey_death_p)
233+
self.grid[prey_death_cells] = 0
234+
235+
# Predator death
236+
pred_death_p = float(self.params["predator_death"])
237+
rand = self.generator.random(size=(rows, cols))
238+
pred_death_cells = (old == 2) & (rand < pred_death_p)
239+
self.grid[pred_death_cells] = 0
240+

prompts.md

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,115 @@ class CA:
157157
Removed a few unnecessary lines.
158158

159159
## Add update logic
160-
Now the basic predator-prey update rules need to be defined. This is done
160+
Now the basic predator-prey update rules need to be defined. This is done by creating a predator-prey class inheriting the CA class and overriding the update function.
161+
162+
### prompt
163+
Create a predator-prey class named PP which inherits the CA class. Create default values for the following parameters: prey_death, predator_death, reproduction, consumption. Make sure not to override the user-set values for these parameters. Also raise an error if there are any other user-defined parameters and check whether the set parameters are valid (between 0 and 1).
164+
165+
Next, override the update function with the following functionality. First, create a copy of the current grid to be used for all the rules. Then, use the inherited count_neighbors function. For each empty cell with at least one neighboring prey, populate it with a prey (state 1) with a random chance. Use generator.random() < 1 - (1 - birth)**prey_neighbors to simulate the possibility for each neighboring prey to reproduce into the cell. For each prey cell in the copied grid with at least one neighboring predator, replace it with a predator with a random chance. use the same logic for this. Afterwards, handle the prey and predator deaths stochastically based on the relevant parameter. Use the copied grid for this to ensure newly occupied cells dont instantly become empty.
166+
167+
### output
168+
class PP(CA):
169+
"""Predator-Prey cellular automaton.
170+
171+
States: 0 = empty, 1 = prey, 2 = predator
172+
173+
Expected params keys (all values in [0,1]):
174+
- "prey_death": probability a prey dies each step
175+
- "predator_death": probability a predator dies each step
176+
- "reproduction": per-neighbor prey reproduction probability
177+
- "consumption": per-neighbor predation probability
178+
179+
Defaults are provided for any missing keys and user-provided values
180+
are preserved. Any unknown keys in `params` will raise a ValueError.
181+
"""
182+
183+
def __init__(
184+
self,
185+
rows: int,
186+
cols: int,
187+
densities: Tuple[float, ...],
188+
neighborhood: str,
189+
params: Dict[str, object],
190+
cell_params: Dict[str, object],
191+
seed: Optional[int] = None,
192+
) -> None:
193+
# initialize base CA
194+
super().__init__(rows, cols, densities, neighborhood, params, cell_params, seed)
195+
196+
# Enforce predator-prey has exactly two species
197+
assert self.n_species == 2, "PP model requires exactly two species (prey=1, predator=2)"
198+
199+
# Allowed parameter keys and defaults
200+
_allowed = {
201+
"prey_death": 0.02,
202+
"predator_death": 0.05,
203+
"reproduction": 0.2,
204+
"consumption": 0.5,
205+
}
206+
207+
# Check for unknown user-specified keys (in the params dict provided by user)
208+
user_keys = set(self.params.keys())
209+
unknown = user_keys.difference(_allowed.keys())
210+
if len(unknown) > 0:
211+
raise ValueError(f"Unknown parameter keys for PP: {sorted(list(unknown))}")
212+
213+
# Fill defaults for missing keys without overriding user-specified values
214+
for k, v in _allowed.items():
215+
if k not in self.params:
216+
self.params[k] = v
217+
218+
# Validate parameter ranges
219+
for k in _allowed.keys():
220+
val = self.params[k]
221+
if not isinstance(val, (int, float)):
222+
raise TypeError(f"Parameter '{k}' must be numeric")
223+
if not (0.0 <= float(val) <= 1.0):
224+
raise ValueError(f"Parameter '{k}' must be between 0 and 1 (got {val})")
225+
226+
def update(self) -> None:
227+
"""One update step for predator-prey dynamics.
228+
229+
Uses a copy of the current grid to evaluate rules so newly changed
230+
cells do not immediately influence other rules in the same step.
231+
"""
232+
# copy of the grid to base all decisions on
233+
old = self.grid.copy()
234+
235+
# neighbor counts for each species (index 0 -> prey, 1 -> predator)
236+
counts = self.count_neighbors()
237+
prey_neighbors = counts[0]
238+
pred_neighbors = counts[1] if self.n_species >= 2 else np.zeros_like(self.grid)
239+
240+
rows, cols = self.grid.shape
241+
# Reproduction into empty cells from neighboring prey
242+
empty_mask = old == 0
243+
# probability that at least one neighboring prey reproduces into the cell
244+
birth_param = float(self.params["reproduction"])
245+
birth_prob = 1.0 - np.power(1.0 - birth_param, prey_neighbors)
246+
rand = self.generator.random(size=(rows, cols))
247+
birth_cells = empty_mask & (prey_neighbors > 0) & (rand < birth_prob)
248+
self.grid[birth_cells] = 1
249+
250+
# Predation: prey replaced by predator due to neighboring predators
251+
prey_mask = old == 1
252+
cons_param = float(self.params["consumption"])
253+
cons_prob = 1.0 - np.power(1.0 - cons_param, pred_neighbors)
254+
rand = self.generator.random(size=(rows, cols))
255+
predation_cells = prey_mask & (pred_neighbors > 0) & (rand < cons_prob)
256+
self.grid[predation_cells] = 2
257+
258+
# Deaths: use the copied `old` grid so newly-occupied cells are not killed immediately
259+
# Prey death
260+
prey_death_p = float(self.params["prey_death"])
261+
rand = self.generator.random(size=(rows, cols))
262+
prey_death_cells = (old == 1) & (rand < prey_death_p)
263+
self.grid[prey_death_cells] = 0
264+
265+
# Predator death
266+
pred_death_p = float(self.params["predator_death"])
267+
rand = self.generator.random(size=(rows, cols))
268+
pred_death_cells = (old == 2) & (rand < pred_death_p)
269+
self.grid[pred_death_cells] = 0
270+
271+
### changes

test.ipynb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 20,
6+
"id": "fcec2b3a",
7+
"metadata": {},
8+
"outputs": [
9+
{
10+
"name": "stdout",
11+
"output_type": "stream",
12+
"text": [
13+
"766\n",
14+
"765\n"
15+
]
16+
}
17+
],
18+
"source": [
19+
"import numpy as np\n",
20+
"x = np.random.rand(1000, 5)\n",
21+
"print(sum(x.min(axis=1) < 0.25))\n",
22+
"y = np.random.rand(1000)\n",
23+
"print(sum(y < 1 - (1 - 0.25)**5))"
24+
]
25+
}
26+
],
27+
"metadata": {
28+
"kernelspec": {
29+
"display_name": "base",
30+
"language": "python",
31+
"name": "python3"
32+
},
33+
"language_info": {
34+
"codemirror_mode": {
35+
"name": "ipython",
36+
"version": 3
37+
},
38+
"file_extension": ".py",
39+
"mimetype": "text/x-python",
40+
"name": "python",
41+
"nbconvert_exporter": "python",
42+
"pygments_lexer": "ipython3",
43+
"version": "3.13.9"
44+
}
45+
},
46+
"nbformat": 4,
47+
"nbformat_minor": 5
48+
}

0 commit comments

Comments
 (0)