|
8 | 8 |
|
9 | 9 | import numpy as np |
10 | 10 | import logging |
11 | | -from scripts.numba_optimized import PPKernel |
12 | | -from numba import njit |
| 11 | +from scripts.numba_optimized import PPKernel, set_numba_seed |
13 | 12 |
|
14 | 13 | # Module logger |
15 | 14 | logger = logging.getLogger(__name__) |
|
19 | 18 | _cached_ndimage = None |
20 | 19 | _cached_kernels = {} |
21 | 20 |
|
22 | | -@njit |
23 | | -def set_numba_seed(value): |
24 | | - np.random.seed(value) |
25 | 21 |
|
26 | 22 | class CA: |
27 | 23 | """Base cellular automaton class. |
@@ -852,6 +848,7 @@ def __init__( |
852 | 848 | cell_params: Dict[str, object] = None, |
853 | 849 | seed: Optional[int] = None, |
854 | 850 | synchronous: bool = True, |
| 851 | + directed_hunting: bool = False, # New directed hunting option |
855 | 852 | ) -> None: |
856 | 853 | # Allowed params and defaults |
857 | 854 | _defaults = { |
@@ -885,14 +882,16 @@ def __init__( |
885 | 882 | super().__init__(rows, cols, densities, neighborhood, merged_params, cell_params, seed) |
886 | 883 |
|
887 | 884 | self.synchronous: bool = bool(synchronous) |
| 885 | + self.directed_hunting: bool = bool(directed_hunting) |
| 886 | + |
888 | 887 | # set human-friendly species names for PP |
889 | 888 | self.species_names = ("prey", "predator") |
890 | 889 |
|
891 | 890 | if seed is not None: |
892 | 891 | # This sets the seed for all @njit functions globally |
893 | 892 | set_numba_seed(seed) |
894 | 893 |
|
895 | | - self._kernel = PPKernel(rows, cols, neighborhood) |
| 894 | + self._kernel = PPKernel(rows, cols, neighborhood, directed_hunting=directed_hunting) |
896 | 895 |
|
897 | 896 |
|
898 | 897 | # 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 |
1167 | 1166 |
|
1168 | 1167 | # Handle inheritance/clearing of per-cell parameters |
1169 | 1168 | 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 |
1170 | 1179 |
|
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) |
1174 | 1183 |
|
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 |
1176 | 1257 | 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) |
1178 | 1262 |
|
1179 | 1263 | def update_async(self) -> None: |
1180 | 1264 | # Get the evolved prey death map |
|
0 commit comments