Skip to content

Commit a0d168f

Browse files
committed
added firebreak feature structure
1 parent f33cbab commit a0d168f

5 files changed

Lines changed: 233 additions & 3 deletions

File tree

docs/prompts.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,8 @@ In the run function, check whether the entries in the cell_params arrays align w
545545
### output
546546
Made large changes to multiple functions. See git commit for details.
547547

548+
---
549+
548550

549551
### Mean Field class
550552

@@ -562,4 +564,9 @@ Made large changes to multiple functions. See git commit for details.
562564

563565
7. Create a bifuracation diagram to confirm the monotonic relationship for a varying prey death rate vs. equilibrium density.
564566

565-
###
567+
---
568+
569+
### Testing CA class
570+
571+
572+
1. Create a comprehensive testing suite for the CA and PP classes. Test initialization, async update changes, synchronous update changes, prey growth in isolation behavior, predator starvation, parameter evolution and long run dynamics. Also make sur ethe test_viz mehtod works as desired

models/CA.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,11 +592,11 @@ def _process_reproduction(sources, birth_param_key, birth_prob, target_state_req
592592

593593
# Prey reproduce into empty cells (target state 0 -> new state 1)
594594
prey_sources = np.argwhere(grid_ref == 1)
595-
_process_reproduction(prey_sources, self.params["prey_birth"], 0, 1)
595+
_process_reproduction(prey_sources, "prey_birth", self.params["prey_birth"], 0, 1)
596596

597597
# Predators reproduce into prey cells (target state 1 -> new state 2)
598598
pred_sources = np.argwhere(grid_ref == 2)
599-
_process_reproduction(pred_sources, self.params["predator_birth"], 1, 2)
599+
_process_reproduction(pred_sources, "predator_birth", self.params["predator_birth"], 1, 2)
600600

601601
def update_async(self) -> None:
602602
"""Asynchronous (random-sequential) update.

models/firebreak.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
""""
2+
Altruistic Suicide Firebreak Model
3+
4+
Concept:
5+
6+
- Standard Prey: Dies of old age or being eaten
7+
- Firebreak prey: Has a sensor to detect if a predator is nearby. If so, then it has
8+
the option to protect the colony by commiting suicde before the predator can reproduce
9+
into it
10+
- Result: Creates empty cells around the predator cluster. Prevents percolation through
11+
the cluster
12+
13+
14+
TO DO:
15+
16+
1. Override update_sync and update_async logic.
17+
New Logic:
18+
1. Calculate predator_neighbors for every cell
19+
2. Create a death rate grid where every cell gets assigned a death rate based on neighbors
20+
3. Calc the mask against the dynamic grid
21+
"""
22+
23+
import numpy as np
24+
from models.CA import PP
25+
26+
27+
class Firebreak(PP):
28+
"""
29+
PP CA where prey commit suicide when threatend to creaty empty firebreaks
30+
that starve predators
31+
"""
32+
33+
def __init__(self, rows, cols, densities, neighborhood = "moore",
34+
params = None, cell_params = None, seed = None, synchronous = True):
35+
# get firebreak specific parameter
36+
self.alt_dr = 0.5 #FIXME: Random default value
37+
clean_params = dict(params) if params else {}
38+
if "altruistic_dr" in clean_params:
39+
self.alt_dr = float(clean_params.pop("altruistic_dr"))
40+
41+
super().__init__(rows, cols, densities, neighborhood, clean_params, cell_params, seed, synchronous)
42+
self.params['altruistic_dr'] = self.alt_dr
43+
44+
def update_sync(self) -> None:
45+
"""Override syncrhonous update. Similar to PP update except Prey Death Logic."""
46+
pass
47+
48+

notebooks/mf.ipynb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
{
22
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"### Mean Field Model"
8+
]
9+
},
310
{
411
"cell_type": "code",
512
"execution_count": 2,

tests/test_pp.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import pytest
2+
import numpy as np
3+
import sys
4+
import os
5+
6+
# Ensure we can import the model from the current directory
7+
sys.path.append(os.getcwd())
8+
9+
# Try importing the classes; fail gracefully if file is missing
10+
try:
11+
from models.CA import PP
12+
except ImportError:
13+
pytest.fail("Could not import 'PP' from 'ca_model.py'. Make sure the file exists.")
14+
15+
# --- FIXTURES ---
16+
17+
@pytest.fixture
18+
def base_params():
19+
"""Standard robust parameters for testing."""
20+
return {
21+
"prey_death": 0.05,
22+
"predator_death": 0.1,
23+
"prey_birth": 0.25,
24+
"predator_birth": 0.2,
25+
}
26+
27+
@pytest.fixture
28+
def seed():
29+
"""Fixed seed for reproducibility."""
30+
return 42
31+
32+
# --- TESTS ---
33+
34+
def test_initialization(base_params, seed):
35+
"""Test grid setup, shapes, and density distribution."""
36+
rows, cols = 50, 50
37+
densities = (0.2, 0.1) # 20% prey, 10% predator
38+
39+
pp = PP(rows, cols, densities, params=base_params, seed=seed)
40+
41+
# Check grid dimensions
42+
assert pp.grid.shape == (rows, cols)
43+
44+
# Check population counts (approximate)
45+
total_cells = rows * cols
46+
prey_count = np.sum(pp.grid == 1)
47+
pred_count = np.sum(pp.grid == 2)
48+
49+
# Allow small variance due to randomness
50+
tolerance = total_cells * 0.05
51+
assert abs(prey_count - total_cells * 0.2) < tolerance
52+
assert abs(pred_count - total_cells * 0.1) < tolerance
53+
54+
def test_async_update_changes_grid(base_params, seed):
55+
"""Test if Asynchronous update actually modifies the grid."""
56+
pp = PP(20, 20, (0.5, 0.2), synchronous=False, params=base_params, seed=seed)
57+
initial_grid = pp.grid.copy()
58+
59+
pp.update()
60+
61+
# In a generic CA step with these densities, the grid MUST change
62+
assert not np.array_equal(pp.grid, initial_grid), "Grid did not change after Async update"
63+
64+
def test_sync_update_changes_grid(base_params, seed):
65+
"""Test if Synchronous update actually modifies the grid."""
66+
pp = PP(20, 20, (0.5, 0.2), synchronous=True, params=base_params, seed=seed)
67+
initial_grid = pp.grid.copy()
68+
69+
pp.update()
70+
71+
assert not np.array_equal(pp.grid, initial_grid), "Grid did not change after Sync update"
72+
73+
def test_prey_growth_in_isolation(seed):
74+
"""Prey should grow if there are no predators and high birth rate."""
75+
growth_params = {
76+
"prey_death": 0.0,
77+
"predator_death": 1.0, # Kill any accidental predators
78+
"prey_birth": 1.0, # Max birth rate
79+
"predator_birth": 0.0,
80+
}
81+
# Start with only prey (10%)
82+
pp = PP(20, 20, (0.1, 0.0), params=growth_params, synchronous=True, seed=seed)
83+
84+
start_count = np.sum(pp.grid == 1)
85+
pp.update()
86+
end_count = np.sum(pp.grid == 1)
87+
88+
assert end_count > start_count, "Prey did not grow in isolation"
89+
90+
def test_predator_starvation(seed):
91+
"""Predators should die if there is no prey."""
92+
starve_params = {
93+
"prey_death": 0.0,
94+
"predator_death": 0.5, # High death rate
95+
"prey_birth": 0.0,
96+
"predator_birth": 1.0,
97+
}
98+
# Start with only predators (50%)
99+
pp = PP(20, 20, (0.0, 0.5), params=starve_params, synchronous=True, seed=seed)
100+
101+
start_count = np.sum(pp.grid == 2)
102+
pp.update()
103+
end_count = np.sum(pp.grid == 2)
104+
105+
assert end_count < start_count, "Predators did not die from starvation"
106+
107+
def test_parameter_evolution(base_params, seed):
108+
"""Test if per-cell parameters initialize and mutate correctly."""
109+
pp = PP(30, 30, (0.3, 0.1), params=base_params, seed=seed)
110+
111+
# Enable evolution for 'prey_death'
112+
pp.evolve("prey_death", sd=0.05)
113+
114+
# Check key existence
115+
assert "prey_death" in pp.cell_params
116+
117+
# Check initialization logic
118+
param_grid = pp.cell_params["prey_death"]
119+
prey_mask = (pp.grid == 1)
120+
121+
# Values should exist where prey exists
122+
assert np.all(~np.isnan(param_grid[prey_mask]))
123+
# Values should be NaN where prey does NOT exist
124+
assert np.all(np.isnan(param_grid[~prey_mask]))
125+
126+
# Run updates to force reproduction and mutation
127+
for _ in range(5):
128+
pp.update()
129+
130+
# Check for parameter drift (variance)
131+
current_vals = pp.cell_params["prey_death"]
132+
valid_vals = current_vals[~np.isnan(current_vals)]
133+
134+
# If mutation is working, we expect the values to diverge from the initial scalar
135+
if len(valid_vals) > 5:
136+
assert np.std(valid_vals) > 0.0, "Parameters did not mutate/drift (variance is 0)"
137+
138+
def test_stability_long_run(base_params, seed):
139+
"""Run for 100 steps to ensure no immediate crash/extinction with default params."""
140+
pp = PP(50, 50, (0.2, 0.1), synchronous=True, params=base_params, seed=seed)
141+
142+
extinct = False
143+
for _ in range(100):
144+
pp.update()
145+
n_prey = np.sum(pp.grid == 1)
146+
n_pred = np.sum(pp.grid == 2)
147+
148+
# We consider 'extinct' if either species drops to 0
149+
if n_prey == 0 or n_pred == 0:
150+
extinct = True
151+
break
152+
153+
assert not extinct, "Populations went extinct within 100 steps with default parameters"
154+
155+
def test_viz_smoke_test():
156+
"""Ensure visualize() can be called without error (requires matplotlib)."""
157+
try:
158+
import matplotlib.pyplot as plt
159+
except ImportError:
160+
pytest.skip("Matplotlib not installed")
161+
162+
try:
163+
pp = PP(10, 10, (0.2, 0.1))
164+
# Just initialize visualization, don't keep window open
165+
pp.visualize(interval=1, pause=0.001)
166+
plt.close('all') # Cleanup figures
167+
except Exception as e:
168+
pytest.fail(f"visualize() raised an exception: {e}")

0 commit comments

Comments
 (0)