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