Skip to content

Commit 40ee38a

Browse files
committed
directed hunting functionality with numba. Still need to udpate the analsyis file
1 parent 15ebbff commit 40ee38a

2 files changed

Lines changed: 271 additions & 57 deletions

File tree

models/CA.py

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88

99
import numpy as np
1010
import logging
11-
from scripts.numba_optimized import PPKernel
12-
from numba import njit
11+
from scripts.numba_optimized import PPKernel, set_numba_seed
1312

1413
# Module logger
1514
logger = logging.getLogger(__name__)
@@ -19,9 +18,6 @@
1918
_cached_ndimage = None
2019
_cached_kernels = {}
2120

22-
@njit
23-
def set_numba_seed(value):
24-
np.random.seed(value)
2521

2622
class CA:
2723
"""Base cellular automaton class.
@@ -852,6 +848,7 @@ def __init__(
852848
cell_params: Dict[str, object] = None,
853849
seed: Optional[int] = None,
854850
synchronous: bool = True,
851+
directed_hunting: bool = False, # New directed hunting option
855852
) -> None:
856853
# Allowed params and defaults
857854
_defaults = {
@@ -885,14 +882,16 @@ def __init__(
885882
super().__init__(rows, cols, densities, neighborhood, merged_params, cell_params, seed)
886883

887884
self.synchronous: bool = bool(synchronous)
885+
self.directed_hunting: bool = bool(directed_hunting)
886+
888887
# set human-friendly species names for PP
889888
self.species_names = ("prey", "predator")
890889

891890
if seed is not None:
892891
# This sets the seed for all @njit functions globally
893892
set_numba_seed(seed)
894893

895-
self._kernel = PPKernel(rows, cols, neighborhood)
894+
self._kernel = PPKernel(rows, cols, neighborhood, directed_hunting=directed_hunting)
896895

897896

898897
# Remove PP-specific evolve wrapper; use CA.evolve with optional species
@@ -1167,14 +1166,99 @@ def _process_reproduction(sources, birth_param_key, birth_prob, target_state_req
11671166

11681167
# Handle inheritance/clearing of per-cell parameters
11691168
self._inherit_params_on_birth(chosen_rs, chosen_cs, parents, new_state_val)
1169+
1170+
1171+
def _process_predator_hunting(sources, birth_param_key, birth_prob):
1172+
"""Handle predator reproduction with directed movement toward prey.
1173+
1174+
Predators check all neighbors: if any neighbor contains prey,
1175+
preferentially move to one of them; otherwise pick a random neighbor.
1176+
"""
1177+
if sources.size == 0:
1178+
return
11701179

1171-
# Prey reproduce into empty cells (target state 0 -> new state 1)
1172-
prey_sources = np.argwhere(grid_ref == 1)
1173-
_process_reproduction(prey_sources, "prey_birth", self.params["prey_birth"], 0, 1)
1180+
M = sources.shape[0]
1181+
# Determine per-source birth probabilities (from cell_params if present)
1182+
parent_probs = self._get_parent_probs(sources, birth_param_key, birth_prob)
11741183

1175-
# Predators reproduce into prey cells (target state 1 -> new state 2)
1184+
# Which sources attempt reproduction
1185+
attempt_mask = gen.random(M) < parent_probs
1186+
if not np.any(attempt_mask):
1187+
return
1188+
1189+
src = sources[attempt_mask]
1190+
K = src.shape[0]
1191+
1192+
# For each predator, check all neighbors to find prey
1193+
selected_neighbors = np.zeros((K, 2), dtype=int)
1194+
1195+
for i in range(K):
1196+
r, c = int(src[i, 0]), int(src[i, 1])
1197+
# Get all neighbor positions
1198+
neighbors_r = (r + dr_arr) % rows
1199+
neighbors_c = (c + dc_arr) % cols
1200+
# Check which neighbors have prey
1201+
prey_neighbors = (grid_ref[neighbors_r, neighbors_c] == 1)
1202+
1203+
if np.any(prey_neighbors):
1204+
# Pick one prey neighbor uniformly at random (directed movement)
1205+
prey_indices = np.where(prey_neighbors)[0]
1206+
chosen_idx = int(gen.choice(prey_indices))
1207+
else:
1208+
# No prey visible; pick a random neighbor
1209+
chosen_idx = int(gen.integers(0, n_shifts))
1210+
1211+
selected_neighbors[i, 0] = neighbors_r[chosen_idx]
1212+
selected_neighbors[i, 1] = neighbors_c[chosen_idx]
1213+
1214+
nr = selected_neighbors[:, 0]
1215+
nc = selected_neighbors[:, 1]
1216+
1217+
# Only keep attempts where the target was prey (required state = 1)
1218+
valid_mask = (grid_ref[nr, nc] == 1)
1219+
if not np.any(valid_mask):
1220+
return
1221+
1222+
src_valid = src[valid_mask]
1223+
nr = nr[valid_mask]
1224+
nc = nc[valid_mask]
1225+
1226+
# Flatten target indices to group collisions
1227+
target_flat = (nr * cols + nc).astype(np.int64)
1228+
order = np.argsort(target_flat)
1229+
tf_sorted = target_flat[order]
1230+
1231+
uniq_targets, idx_start, counts = np.unique(tf_sorted, return_index=True, return_counts=True)
1232+
if uniq_targets.size == 0:
1233+
return
1234+
1235+
# For each unique target, pick one predator uniformly at random
1236+
chosen_sorted_positions = []
1237+
for start, cnt in zip(idx_start, counts):
1238+
off = int(gen.integers(0, cnt))
1239+
chosen_sorted_positions.append(start + off)
1240+
chosen_sorted_positions = np.array(chosen_sorted_positions, dtype=int)
1241+
1242+
chosen_indices = order[chosen_sorted_positions]
1243+
chosen_target_flats = target_flat[chosen_indices]
1244+
chosen_rs = (chosen_target_flats // cols).astype(int)
1245+
chosen_cs = (chosen_target_flats % cols).astype(int)
1246+
1247+
parents = src_valid[chosen_indices]
1248+
1249+
# Apply successful hunts: predators convert prey to predator
1250+
grid[chosen_rs, chosen_cs] = 2
1251+
1252+
# Handle inheritance/clearing of per-cell parameters
1253+
self._inherit_params_on_birth(chosen_rs, chosen_cs, parents, 2)
1254+
1255+
1256+
# Predators hunt with directed movement toward prey
11761257
pred_sources = np.argwhere(grid_ref == 2)
1177-
_process_reproduction(pred_sources, "predator_birth", self.params["predator_birth"], 1, 2)
1258+
if self.directed_hunting:
1259+
_process_predator_hunting(pred_sources, "predator_birth", self.params["predator_birth"])
1260+
else:
1261+
_process_reproduction(pred_sources, "predator_birth", self.params["predator_birth"], 1, 2)
11781262

11791263
def update_async(self) -> None:
11801264
# Get the evolved prey death map

0 commit comments

Comments
 (0)