Skip to content

Commit 8f9effa

Browse files
committed
CA and PP testing
1 parent be90954 commit 8f9effa

3 files changed

Lines changed: 218 additions & 14 deletions

File tree

docs/prompts.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,122 @@ 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+
## CA and PP tests
549+
Now that the MVP functionality is fulfilled, we need to test it. For this, we create numerous tests for both the CA class and the PP class, testing if incorrect arguments raise the correct errors, if any functions raise unwanted errors, but mostly if the update functions work as intended.
550+
551+
### prompt
552+
Add tests for the CA and PP class to the test_ca.py file. Check whether incorrect arguments do not raise errors and if any functions do raise errors. Also, test whether run picks up on cell_params array that do not align by grid. For this, manually change some cell_params array entries before running the simulation. Check whether all errors are informative, in the sense that a user would know how to avoid it next time.
553+
554+
Lastly, check whether trivial simulations result in the correct outcome. For example, if both the predator and prey death rates are 1 and the birth rates are below 1, then the grid should go extinct. Also, if the birth rate of the prey and predators are 1 and the death rate of the predators to be 0 we expect the prey to go extinct an the predators to occupy a large number of cells.
555+
556+
Create any additional tests if they are unique, meaning it is not tested already, and well-defined, meaning we know the intended behavior and we know how to compare it to the true behavior.
557+
558+
### output
559+
def test_initialization_and_grid_filling():
560+
rows, cols = 10, 10
561+
densities = (0.2, 0.1)
562+
ca = CA(rows, cols, densities, neighborhood="moore", params={}, cell_params={}, seed=42)
563+
assert ca.grid.shape == (rows, cols)
564+
assert ca.n_species == len(densities)
565+
total_cells = rows * cols
566+
# expected counts use the same rounding as CA.__init__
567+
expected_counts = [int(round(total_cells * d)) for d in densities]
568+
# verify actual counts equal expected
569+
for i, exp in enumerate(expected_counts, start=1):
570+
assert int(np.count_nonzero(ca.grid == i)) == exp
571+
572+
573+
def test_invalid_parameters_raise():
574+
# invalid rows/cols
575+
with pytest.raises(AssertionError):
576+
CA(0, 5, (0.1,), "moore", {}, {}, seed=1)
577+
with pytest.raises(AssertionError):
578+
CA(5, -1, (0.1,), "moore", {}, {}, seed=1)
579+
# densities must be non-empty tuple
580+
with pytest.raises(AssertionError):
581+
CA(5, 5, (), "moore", {}, {}, seed=1)
582+
# densities sum > 1
583+
with pytest.raises(AssertionError):
584+
CA(5, 5, (0.8, 0.8), "moore", {}, {}, seed=1)
585+
# invalid neighborhood
586+
with pytest.raises(AssertionError):
587+
CA(5, 5, (0.1,), "invalid", {}, {}, seed=1)
588+
589+
# PP: params must be a dict or None
590+
with pytest.raises(TypeError):
591+
PP(rows=5, cols=5, densities=(0.2, 0.1), neighborhood="moore", params="bad", cell_params=None, seed=1)
592+
593+
594+
def test_neighborhood_counting():
595+
# set up a small grid with a single prey in the center and check neighbor counts
596+
ca = CA(3, 3, (0.0,), neighborhood="moore", params={}, cell_params={}, seed=1)
597+
ca.grid[:] = 0
598+
ca.grid[1, 1] = 1
599+
counts = ca.count_neighbors()
600+
# counts is a tuple with one array (state 1)
601+
neigh = counts[0]
602+
# all 8 neighbors of center should have count 1
603+
expected_positions = [(0, 0), (0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1), (2, 2)]
604+
for r in range(3):
605+
for c in range(3):
606+
if (r, c) in expected_positions:
607+
assert neigh[r, c] == 1
608+
else:
609+
# center has 0 neighbors of same state
610+
assert neigh[r, c] == 0
611+
612+
613+
def test_run_detects_cell_params_shape_and_nonnan_mismatch():
614+
# create a PP and enable evolution for a parameter
615+
pp = PP(rows=5, cols=5, densities=(0.2, 0.1), neighborhood="moore", params=None, cell_params=None, seed=2)
616+
pp.evolve("prey_death", sd=0.01, min=0.0, max=1.0)
617+
618+
# wrong shape should raise informative ValueError during run()
619+
pp.cell_params["prey_death"] = np.zeros((1, 1))
620+
with pytest.raises(ValueError) as excinfo:
621+
pp.run(1)
622+
assert "shape equal to grid" in str(excinfo.value)
623+
624+
# now create a same-shaped array but with non-NaN positions that don't match prey positions
625+
arr = np.zeros(pp.grid.shape, dtype=float) # filled with non-NaN everywhere
626+
pp.cell_params["prey_death"] = arr
627+
with pytest.raises(ValueError) as excinfo2:
628+
pp.run(1)
629+
assert "non-NaN entries must match positions" in str(excinfo2.value)
630+
631+
632+
def test_extinction_when_death_one():
633+
# when both death rates are 1 all individuals should die in one step
634+
params = {
635+
"prey_death": 1.0,
636+
"predator_death": 1.0,
637+
"prey_birth": 0.0,
638+
"predator_birth": 0.0,
639+
}
640+
pp = PP(rows=10, cols=10, densities=(0.2, 0.1), neighborhood="moore", params=params, cell_params=None, seed=3)
641+
pp.run(1)
642+
# no prey or predators should remain
643+
assert np.count_nonzero(pp.grid != 0) == 0
644+
645+
646+
def test_predators_dominate_with_high_birth_and_zero_predator_death():
647+
params = {
648+
"prey_death": 0.0,
649+
"predator_death": 0.0,
650+
"prey_birth": 1.0,
651+
"predator_birth": 1.0,
652+
}
653+
pp = PP(rows=10, cols=10, densities=(0.1, 0.05), neighborhood="moore", params=params, cell_params=None, seed=4)
654+
# run longer to allow predators to consume prey; expect prey extinction
655+
pp.run(200)
656+
after_prey = int(np.count_nonzero(pp.grid == 1))
657+
after_pred = int(np.count_nonzero(pp.grid == 2))
658+
# after sufficient time, prey should go extinct and predators remain
659+
assert after_prey == 0
660+
assert after_pred > 0
661+
662+
### changes
663+
Revealed and fixed error in the PP class' update functions where no parameter key was passed to the _process_reproduction function, resulting in an error.
548664

549665
### Mean Field class
550666

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.

tests/test_ca.py

Lines changed: 100 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,108 @@
22

33
import pytest
44
import numpy as np
5-
from models.CA import CA
5+
from models.CA import CA, PP
66

7-
# @pytest.fixture
8-
def ca_instance():
9-
pass
107

11-
def test_initialization():
12-
pass
8+
def test_initialization_and_grid_filling():
9+
rows, cols = 10, 10
10+
densities = (0.2, 0.1)
11+
ca = CA(rows, cols, densities, neighborhood="moore", params={}, cell_params={}, seed=42)
12+
assert ca.grid.shape == (rows, cols)
13+
assert ca.n_species == len(densities)
14+
total_cells = rows * cols
15+
# expected counts use the same rounding as CA.__init__
16+
expected_counts = [int(round(total_cells * d)) for d in densities]
17+
# verify actual counts equal expected
18+
for i, exp in enumerate(expected_counts, start=1):
19+
assert int(np.count_nonzero(ca.grid == i)) == exp
1320

14-
def test_invalid_parameters():
15-
pass
1621

17-
def test_grid_filling():
18-
pass
22+
def test_invalid_parameters_raise():
23+
# invalid rows/cols
24+
with pytest.raises(AssertionError):
25+
CA(0, 5, (0.1,), "moore", {}, {}, seed=1)
26+
with pytest.raises(AssertionError):
27+
CA(5, -1, (0.1,), "moore", {}, {}, seed=1)
28+
# densities must be non-empty tuple
29+
with pytest.raises(AssertionError):
30+
CA(5, 5, (), "moore", {}, {}, seed=1)
31+
# densities sum > 1
32+
with pytest.raises(AssertionError):
33+
CA(5, 5, (0.8, 0.8), "moore", {}, {}, seed=1)
34+
# invalid neighborhood
35+
with pytest.raises(AssertionError):
36+
CA(5, 5, (0.1,), "invalid", {}, {}, seed=1)
1937

20-
def test_neighborhood_types():
21-
pass
38+
# PP: params must be a dict or None
39+
with pytest.raises(TypeError):
40+
PP(rows=5, cols=5, densities=(0.2, 0.1), neighborhood="moore", params="bad", cell_params=None, seed=1)
41+
42+
43+
def test_neighborhood_counting():
44+
# set up a small grid with a single prey in the center and check neighbor counts
45+
ca = CA(3, 3, (0.0,), neighborhood="moore", params={}, cell_params={}, seed=1)
46+
ca.grid[:] = 0
47+
ca.grid[1, 1] = 1
48+
counts = ca.count_neighbors()
49+
# counts is a tuple with one array (state 1)
50+
neigh = counts[0]
51+
# all 8 neighbors of center should have count 1
52+
expected_positions = [(0, 0), (0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1), (2, 2)]
53+
for r in range(3):
54+
for c in range(3):
55+
if (r, c) in expected_positions:
56+
assert neigh[r, c] == 1
57+
else:
58+
# center has 0 neighbors of same state
59+
assert neigh[r, c] == 0
60+
61+
62+
def test_run_detects_cell_params_shape_and_nonnan_mismatch():
63+
# create a PP and enable evolution for a parameter
64+
pp = PP(rows=5, cols=5, densities=(0.2, 0.1), neighborhood="moore", params=None, cell_params=None, seed=2)
65+
pp.evolve("prey_death", sd=0.01, min=0.0, max=1.0)
66+
67+
# wrong shape should raise informative ValueError during run()
68+
pp.cell_params["prey_death"] = np.zeros((1, 1))
69+
with pytest.raises(ValueError) as excinfo:
70+
pp.run(1)
71+
assert "shape equal to grid" in str(excinfo.value)
72+
73+
# now create a same-shaped array but with non-NaN positions that don't match prey positions
74+
arr = np.zeros(pp.grid.shape, dtype=float) # filled with non-NaN everywhere
75+
pp.cell_params["prey_death"] = arr
76+
with pytest.raises(ValueError) as excinfo2:
77+
pp.run(1)
78+
assert "non-NaN entries must match positions" in str(excinfo2.value)
79+
80+
81+
def test_extinction_when_death_one():
82+
# when both death rates are 1 all individuals should die in one step
83+
params = {
84+
"prey_death": 1.0,
85+
"predator_death": 1.0,
86+
"prey_birth": 0.0,
87+
"predator_birth": 0.0,
88+
}
89+
pp = PP(rows=10, cols=10, densities=(0.2, 0.1), neighborhood="moore", params=params, cell_params=None, seed=3)
90+
pp.run(1)
91+
# no prey or predators should remain
92+
assert np.count_nonzero(pp.grid != 0) == 0
93+
94+
95+
def test_predators_dominate_with_high_birth_and_zero_predator_death():
96+
params = {
97+
"prey_death": 0.0,
98+
"predator_death": 0.0,
99+
"prey_birth": 1.0,
100+
"predator_birth": 1.0,
101+
}
102+
pp = PP(rows=10, cols=10, densities=(0.1, 0.05), neighborhood="moore", params=params, cell_params=None, seed=4)
103+
# run longer to allow predators to consume prey; expect prey extinction
104+
pp.run(200)
105+
after_prey = int(np.count_nonzero(pp.grid == 1))
106+
after_pred = int(np.count_nonzero(pp.grid == 2))
107+
# after sufficient time, prey should go extinct and predators remain
108+
assert after_prey == 0
109+
assert after_pred > 0

0 commit comments

Comments
 (0)