Skip to content

Commit 1058815

Browse files
committed
tests for numba and analysis files updated to include hunting logic
1 parent 40ee38a commit 1058815

4 files changed

Lines changed: 396 additions & 1 deletion

File tree

docs/kimon_prompts.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,6 @@ Note: Each worker process in parallel execution needs its own seed call.
356356
For parallel simulations, use different seeds per worker (e.g., base_seed + worker_id).
357357

358358

359+
360+
361+
5. Help me write additional tests for the hunting feature logic using the numba kernels. The additional tests will be added to the test_numba and test_pp_analysis test files and should adhere to their exisiting implementation logic. If you falsify tests, you will be replaced.

scripts/pp_analysis.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class Config:
114114

115115
# Update mode
116116
synchronous: bool = False
117+
directed_hunting: bool = False
117118

118119
# Diagnostic snapshots
119120
save_diagnostic_plots: bool = False
@@ -333,6 +334,7 @@ def run_single_simulation(
333334
params=params,
334335
seed=seed,
335336
synchronous=cfg.synchronous,
337+
directed_hunting=cfg.directed_hunting,
336338
)
337339

338340
if with_evolution:
@@ -1024,13 +1026,18 @@ def main():
10241026
parser.add_argument("--cores", type=int, default=-1)
10251027
parser.add_argument("--dry-run", action="store_true")
10261028
parser.add_argument("--sync", action="store_true", dest="synchronous")
1029+
parser.add_argument("--directed-hunting", action="store_true",
1030+
help="Enable directed predator hunting behavior")
10271031
args = parser.parse_args()
10281032

10291033
# Setup
10301034
cfg = Config()
10311035
cfg.synchronous = args.synchronous
1036+
cfg.directed_hunting = getattr(args, 'directed_hunting', False)
10321037
cfg.n_jobs = args.cores if args.cores > 0 else int(os.environ.get("SLURM_CPUS_PER_TASK", -1))
10331038

1039+
warmup_numba_kernels(cfg.default_grid, directed_hunting=cfg.directed_hunting)
1040+
10341041
output_dir = Path(args.output)
10351042
output_dir.mkdir(exist_ok=True)
10361043

@@ -1053,6 +1060,7 @@ def main():
10531060
logger.info(f"Output: {output_dir}")
10541061
logger.info(f"Cores: {cfg.n_jobs}")
10551062
logger.info(f"Numba: {'ENABLED' if USE_NUMBA else 'DISABLED'}")
1063+
logger.info(f"Directed hunting: {'ENABLED' if cfg.directed_hunting else 'DISABLED'}")
10561064

10571065
if args.mode == "debug":
10581066
run_debug_mode(cfg, logger)

tests/test_numba_optimized.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,182 @@ def test_kernel_deterministic_with_seed(self):
258258
assert np.array_equal(results[0], results[1]), "Results should be deterministic"
259259

260260

261+
class TestPPKernelDirectedHunting:
262+
"""Tests for PPKernel with directed hunting behavior."""
263+
264+
def test_kernel_initialization_directed_false(self):
265+
"""Kernel should default to directed_hunting=False."""
266+
kernel = PPKernel(50, 50, "moore")
267+
assert kernel.directed_hunting == False
268+
269+
def test_kernel_initialization_directed_true(self):
270+
"""Kernel should accept directed_hunting=True."""
271+
kernel = PPKernel(50, 50, "moore", directed_hunting=True)
272+
assert kernel.directed_hunting == True
273+
274+
def test_kernel_directed_runs_without_error(self, medium_grid, prey_death_array):
275+
"""Directed hunting kernel should run without errors."""
276+
set_numba_seed(42)
277+
kernel = PPKernel(50, 50, "moore", directed_hunting=True)
278+
279+
grid = medium_grid.copy()
280+
prey_death = prey_death_array.copy()
281+
282+
# Run multiple steps
283+
for _ in range(20):
284+
kernel.update(grid, prey_death, 0.2, 0.05, 0.2, 0.1)
285+
286+
# Grid should only have valid states
287+
assert grid.min() >= 0
288+
assert grid.max() <= 2
289+
290+
def test_kernel_directed_valid_states(self, medium_grid, prey_death_array):
291+
"""Directed kernel should produce only valid states."""
292+
set_numba_seed(42)
293+
kernel = PPKernel(50, 50, "moore", directed_hunting=True)
294+
295+
grid = medium_grid.copy()
296+
prey_death = prey_death_array.copy()
297+
298+
for _ in range(50):
299+
kernel.update(grid, prey_death, 0.2, 0.05, 0.2, 0.1)
300+
301+
unique = np.unique(grid)
302+
assert all(v in [0, 1, 2] for v in unique)
303+
304+
def test_kernel_directed_prey_death_consistency(self, medium_grid, prey_death_array):
305+
"""Directed kernel should maintain prey_death array consistency."""
306+
set_numba_seed(42)
307+
kernel = PPKernel(50, 50, "moore", directed_hunting=True)
308+
309+
grid = medium_grid.copy()
310+
prey_death = prey_death_array.copy()
311+
312+
for _ in range(20):
313+
kernel.update(grid, prey_death, 0.2, 0.05, 0.2, 0.1,
314+
evolution_stopped=False)
315+
316+
# Prey cells should have non-NaN death rates
317+
prey_mask = (grid == 1)
318+
non_prey_mask = (grid != 1)
319+
320+
if np.any(prey_mask):
321+
assert np.all(~np.isnan(prey_death[prey_mask]))
322+
assert np.all(np.isnan(prey_death[non_prey_mask]))
323+
324+
def test_kernel_directed_evolution_respects_bounds(self, medium_grid, prey_death_array):
325+
"""Directed kernel evolution should stay within bounds."""
326+
set_numba_seed(42)
327+
kernel = PPKernel(50, 50, "moore", directed_hunting=True)
328+
evolve_min, evolve_max = 0.01, 0.15
329+
330+
grid = medium_grid.copy()
331+
prey_death = prey_death_array.copy()
332+
333+
for _ in range(100):
334+
kernel.update(grid, prey_death, 0.2, 0.05, 0.2, 0.1,
335+
evolve_sd=0.1, evolve_min=evolve_min, evolve_max=evolve_max,
336+
evolution_stopped=False)
337+
338+
valid_values = prey_death[~np.isnan(prey_death)]
339+
if len(valid_values) > 0:
340+
assert valid_values.min() >= evolve_min - 1e-10
341+
assert valid_values.max() <= evolve_max + 1e-10
342+
343+
def test_kernel_directed_neumann_neighborhood(self):
344+
"""Directed hunting should work with von Neumann neighborhood."""
345+
np.random.seed(42)
346+
set_numba_seed(42)
347+
348+
grid = np.random.choice([0, 1, 2], (30, 30), p=[0.5, 0.3, 0.2]).astype(np.int32)
349+
prey_death = np.full((30, 30), 0.05, dtype=np.float64)
350+
prey_death[grid != 1] = np.nan
351+
352+
kernel = PPKernel(30, 30, "neumann", directed_hunting=True)
353+
354+
for _ in range(20):
355+
kernel.update(grid, prey_death, 0.2, 0.05, 0.2, 0.1)
356+
357+
assert grid.min() >= 0
358+
assert grid.max() <= 2
359+
360+
def test_random_vs_directed_different_behavior(self):
361+
"""Random and directed kernels should produce different results."""
362+
np.random.seed(123)
363+
364+
# Create identical starting grids
365+
grid_template = np.random.choice([0, 1, 2], (40, 40),
366+
p=[0.50, 0.35, 0.15]).astype(np.int32)
367+
368+
grid_random = grid_template.copy()
369+
grid_directed = grid_template.copy()
370+
371+
prey_death_random = np.full((40, 40), 0.05, dtype=np.float64)
372+
prey_death_random[grid_random != 1] = np.nan
373+
prey_death_directed = prey_death_random.copy()
374+
375+
kernel_random = PPKernel(40, 40, "moore", directed_hunting=False)
376+
kernel_directed = PPKernel(40, 40, "moore", directed_hunting=True)
377+
378+
# Run with same seed
379+
set_numba_seed(999)
380+
for _ in range(50):
381+
kernel_random.update(grid_random, prey_death_random,
382+
0.2, 0.05, 0.6, 0.1)
383+
384+
set_numba_seed(999)
385+
for _ in range(50):
386+
kernel_directed.update(grid_directed, prey_death_directed,
387+
0.2, 0.05, 0.6, 0.1)
388+
389+
# Grids should differ (directed hunting changes dynamics)
390+
# Note: not guaranteed for every seed, but highly likely
391+
prey_random = np.sum(grid_random == 1)
392+
prey_directed = np.sum(grid_directed == 1)
393+
pred_random = np.sum(grid_random == 2)
394+
pred_directed = np.sum(grid_directed == 2)
395+
396+
# At minimum, both should have valid grids
397+
assert grid_random.min() >= 0 and grid_random.max() <= 2
398+
assert grid_directed.min() >= 0 and grid_directed.max() <= 2
399+
400+
# The populations should likely differ
401+
# (we don't assert this strictly as it depends on random dynamics)
402+
print(f"Random: prey={prey_random}, pred={pred_random}")
403+
print(f"Directed: prey={prey_directed}, pred={pred_directed}")
404+
405+
def test_directed_predator_hunts_adjacent_prey(self):
406+
"""Directed predator should successfully hunt adjacent prey."""
407+
# Create controlled scenario: predator surrounded by prey
408+
grid = np.zeros((10, 10), dtype=np.int32)
409+
grid[5, 5] = 2 # Predator in center
410+
grid[4, 5] = 1 # Prey above
411+
grid[6, 5] = 1 # Prey below
412+
grid[5, 4] = 1 # Prey left
413+
grid[5, 6] = 1 # Prey right
414+
415+
prey_death = np.full((10, 10), 0.05, dtype=np.float64)
416+
prey_death[grid != 1] = np.nan
417+
418+
kernel = PPKernel(10, 10, "neumann", directed_hunting=True)
419+
420+
initial_prey = np.sum(grid == 1)
421+
initial_pred = np.sum(grid == 2)
422+
423+
# Run with high predator birth, zero predator death
424+
set_numba_seed(42)
425+
for _ in range(5):
426+
kernel.update(grid, prey_death, 0.0, 0.05, 1.0, 0.0)
427+
428+
final_prey = np.sum(grid == 1)
429+
final_pred = np.sum(grid == 2)
430+
431+
# Predators should have converted some prey
432+
# (with 100% birth rate and 0% death rate)
433+
assert final_pred >= initial_pred, "Predator population should not decrease"
434+
print(f"Prey: {initial_prey} -> {final_prey}")
435+
print(f"Pred: {initial_pred} -> {final_pred}")
436+
261437
# ============================================================================
262438
# TEST: PCF COMPUTATION
263439
# ============================================================================
@@ -483,6 +659,14 @@ def test_warmup_compiles_kernel(self):
483659

484660
# Should complete quickly (less than 1 second for 10 iterations)
485661
assert elapsed < 1.0, f"Kernel too slow after warmup: {elapsed:.2f}s"
662+
663+
664+
def test_warmup_directed_hunting(self):
665+
"""Warmup should work with directed_hunting=True."""
666+
try:
667+
warmup_numba_kernels(30, directed_hunting=True)
668+
except Exception as e:
669+
pytest.fail(f"Warmup with directed_hunting failed: {e}")
486670

487671

488672
# ============================================================================
@@ -571,6 +755,44 @@ def test_extreme_parameters(self):
571755
assert True
572756

573757

758+
def test_directed_single_predator_surrounded_by_prey(self):
759+
"""Directed hunting: single predator surrounded by prey."""
760+
grid = np.ones((5, 5), dtype=np.int32) # All prey
761+
grid[2, 2] = 2 # One predator in center
762+
763+
prey_death = np.full((5, 5), 0.05, dtype=np.float64)
764+
prey_death[grid != 1] = np.nan
765+
766+
kernel = PPKernel(5, 5, "moore", directed_hunting=True)
767+
set_numba_seed(42)
768+
769+
# Run a few steps
770+
for _ in range(3):
771+
kernel.update(grid, prey_death, 0.0, 0.05, 0.9, 0.0)
772+
773+
# Should not crash, grid should be valid
774+
assert grid.min() >= 0
775+
assert grid.max() <= 2
776+
777+
def test_directed_no_prey_nearby(self):
778+
"""Directed hunting: predator with no prey neighbors should explore."""
779+
grid = np.zeros((10, 10), dtype=np.int32)
780+
grid[0, 0] = 2 # Predator in corner
781+
grid[9, 9] = 1 # Prey far away
782+
783+
prey_death = np.full((10, 10), 0.05, dtype=np.float64)
784+
prey_death[grid != 1] = np.nan
785+
786+
kernel = PPKernel(10, 10, "moore", directed_hunting=True)
787+
set_numba_seed(42)
788+
789+
# Run - predator should explore randomly (no prey adjacent)
790+
for _ in range(5):
791+
kernel.update(grid, prey_death, 0.0, 0.05, 0.5, 0.0)
792+
793+
assert grid.min() >= 0
794+
assert grid.max() <= 2
795+
574796
# ============================================================================
575797
# MAIN
576798
# ============================================================================

0 commit comments

Comments
 (0)