Directed reproduction and phase 6 config#20
Conversation
Now chooses a random valid neighbor for reproduction instead of a random neighbor (without checking for validity) like in the undirected kernel
Another complete parameter sweep, but with directed reproduction
There was a problem hiding this comment.
Pull request overview
This PR introduces "directed reproduction" behavior to the predator-prey model and updates the Phase 6 configuration. Directed reproduction means that organisms actively choose where to reproduce based on local availability: prey select empty neighboring cells for reproduction, while predators target prey cells. This contrasts with the previous random neighbor selection approach.
Changes:
- Added a new (but unused)
_pp_async_kernel_fastfunction with critical bugs - Modified
_pp_async_kernel_directedto implement directed reproduction instead of directed hunting - Updated Phase 6 configuration to match Phase 4 parameters with directed_hunting flag enabled
- Updated documentation to reflect the change from "directed movement" to "directed reproduction"
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| models/numba_optimized.py | Added unused _pp_async_kernel_fast function (has bugs); modified _pp_async_kernel_directed to implement directed reproduction behavior |
| models/config.py | Updated PHASE6_CONFIG to use Phase 4-like parameters with directed_hunting=True |
| docs/experiments.md | Updated Phase 6 description to mention "directed reproduction" instead of "directed movement" |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| grid = grid_copy | ||
| prey_death_arr = prey_death_arr_copy |
There was a problem hiding this comment.
The function signature indicates it should return a single np.ndarray (the grid), but the function also modifies prey_death_arr_copy. However, these modifications are lost because prey_death_arr_copy is never returned or used. The function should either return a tuple containing both arrays, or it should modify the input arrays in-place like the other kernel functions do.
| grid = grid_copy | |
| prey_death_arr = prey_death_arr_copy | |
| # Copy results back into the original arrays so that updates are visible | |
| grid[:, :] = grid_copy | |
| prey_death_arr[:, :] = prey_death_arr_copy |
| nrs = (rs + dr_arr[nb]) % rows | ||
| ncs = (cs + dc_arr[nb]) % cols | ||
|
|
||
| for r, c, nr, nc in zip(rs, cs, nrs, ncs): |
There was a problem hiding this comment.
The loop processes cells that may have already been marked as dead in grid_copy. The state variable reads from the original grid, but the reproduction logic checks nstate from grid_copy. This creates an inconsistency: a cell that was killed in the death phase (lines 81-86) can still attempt reproduction because its state is read from the original grid. Consider checking if grid_copy[r, c] == 0 before processing reproduction.
| for r, c, nr, nc in zip(rs, cs, nrs, ncs): | |
| for r, c, nr, nc in zip(rs, cs, nrs, ncs): | |
| # Skip cells that have already died during this update step | |
| if grid_copy[r, c] == 0: | |
| continue |
| evolve_min: float, | ||
| evolve_max: float, | ||
| evolution_stopped: bool, | ||
| occupied_buffer: np.ndarray, |
There was a problem hiding this comment.
The occupied_buffer parameter is declared but never used in this function. This is inconsistent with the other kernel functions which use it to track occupied cells. Either the parameter should be removed or the function should be refactored to use it.
| grid: np.ndarray, | ||
| prey_death_arr: np.ndarray, | ||
| p_birth_val: float, | ||
| p_death_val: float, |
There was a problem hiding this comment.
The p_death_val parameter is declared but never used in this function. Based on the pattern in other kernel functions, p_death_val should be used in the prey death logic at line 82, but instead prey_death_arr is used directly. This creates an inconsistency with the function signature and may not match the intended behavior.
| # Phase 6: Model extensions (directed reproduction); same config as phase 4 but with directed reproduction | ||
| PHASE6_CONFIG = Config( | ||
| grid_size=1000, | ||
| n_prey_death=20, | ||
| prey_birth=0.2, | ||
| prey_death_range=(0.09, 0.12), | ||
| predator_birth=0.8, | ||
| predator_death=0.05, | ||
| n_replicates=30, | ||
| warmup_steps=1000, | ||
| measurement_steps=1000, | ||
| collect_pcf=True, | ||
| pcf_sample_rate=0.2, | ||
| save_timeseries=False, | ||
| grid_size=250, | ||
| n_replicates=10, | ||
| warmup_steps=500, | ||
| measurement_steps=500, | ||
| with_evolution=False, | ||
| collect_pcf=False, | ||
| save_timeseries=True, | ||
| timeseries_subsample=10, | ||
| directed_hunting=True, |
There was a problem hiding this comment.
The comment says "directed reproduction" but the parameter name is still "directed_hunting". The PR changes the behavior from directed hunting to directed reproduction, so this parameter should be renamed throughout the codebase to "directed_reproduction" to accurately reflect its purpose. This inconsistency will confuse future developers about what this flag actually does.
| @@ -170,19 +229,22 @@ def _pp_async_kernel_directed( | |||
| c = occupied_buffer[i, 1] | |||
|
|
|||
| state = grid[r, c] | |||
There was a problem hiding this comment.
The state 0 check that was removed is actually needed. Without it, cells that have already been killed (set to 0) earlier in the shuffled iteration order could be processed again when they no longer exist. While the current code will skip processing due to the state checks below, removing this early exit makes the code less efficient and less clear. Consider restoring this check for clarity and efficiency.
| state = grid[r, c] | |
| state = grid[r, c] | |
| if state == 0: | |
| continue |
| def _pp_async_kernel_fast( | ||
| grid: np.ndarray, | ||
| prey_death_arr: np.ndarray, | ||
| p_birth_val: float, | ||
| p_death_val: float, | ||
| pred_birth_val: float, | ||
| pred_death_val: float, | ||
| dr_arr: np.ndarray, | ||
| dc_arr: np.ndarray, | ||
| evolve_sd: float, | ||
| evolve_min: float, | ||
| evolve_max: float, | ||
| evolution_stopped: bool, | ||
| occupied_buffer: np.ndarray, | ||
| ) -> np.ndarray: | ||
| """Partially synchronous predator-prey update kernel.""" | ||
| rows, cols = grid.shape | ||
| n_shifts = len(dr_arr) | ||
| grid_copy = grid.copy() | ||
| prey_death_arr_copy = prey_death_arr.copy() | ||
|
|
||
| prey_death = np.random.random(size=grid.shape) | ||
| grid_copy[(grid == 1) & (prey_death < prey_death_arr)] = 0 | ||
| prey_death_arr_copy[(grid == 1) & (prey_death < prey_death_arr)] = np.nan | ||
|
|
||
| pred_death = np.random.random(size=grid.shape) | ||
| grid_copy[(grid == 2) & (pred_death < pred_death_val)] = 0 | ||
|
|
||
| count = np.count_nonzero(grid) | ||
| indices = np.random.permutation(count) | ||
| rs = indices // cols | ||
| cs = indices % cols | ||
|
|
||
| nb = np.random.randint(0, n_shifts, size=count) | ||
| nrs = (rs + dr_arr[nb]) % rows | ||
| ncs = (cs + dc_arr[nb]) % cols | ||
|
|
||
| for r, c, nr, nc in zip(rs, cs, nrs, ncs): | ||
| state = grid[r, c] | ||
| nstate = grid[nr, nc] | ||
|
|
||
| if state == 1 and nstate == 0 and np.random.random() < p_birth_val: | ||
| grid_copy[nr, nc] = 1 | ||
| parent_val = prey_death_arr[r, c] | ||
| if not evolution_stopped: | ||
| child_val = parent_val + np.random.normal(0, evolve_sd) | ||
| prey_death_arr_copy[nr, nc] = np.clip(child_val, evolve_min, evolve_max) | ||
| else: | ||
| prey_death_arr_copy[nr, nc] = parent_val | ||
|
|
||
| elif state == 2 and nstate == 1 and np.random.random() < pred_birth_val: | ||
| grid_copy[nr, nc] = 2 | ||
| prey_death_arr_copy[nr, nc] = np.nan | ||
|
|
||
| grid = grid_copy | ||
| prey_death_arr = prey_death_arr_copy | ||
|
|
||
| return grid | ||
|
|
||
| @njit(cache=True) |
There was a problem hiding this comment.
This function is defined but never called anywhere in the codebase. It appears to be dead code or work in progress. If this function is not needed, it should be removed. If it's intended for future use, it should be documented or marked as such. Additionally, the function has critical bugs that would prevent it from working correctly.
| def _pp_async_kernel_fast( | |
| grid: np.ndarray, | |
| prey_death_arr: np.ndarray, | |
| p_birth_val: float, | |
| p_death_val: float, | |
| pred_birth_val: float, | |
| pred_death_val: float, | |
| dr_arr: np.ndarray, | |
| dc_arr: np.ndarray, | |
| evolve_sd: float, | |
| evolve_min: float, | |
| evolve_max: float, | |
| evolution_stopped: bool, | |
| occupied_buffer: np.ndarray, | |
| ) -> np.ndarray: | |
| """Partially synchronous predator-prey update kernel.""" | |
| rows, cols = grid.shape | |
| n_shifts = len(dr_arr) | |
| grid_copy = grid.copy() | |
| prey_death_arr_copy = prey_death_arr.copy() | |
| prey_death = np.random.random(size=grid.shape) | |
| grid_copy[(grid == 1) & (prey_death < prey_death_arr)] = 0 | |
| prey_death_arr_copy[(grid == 1) & (prey_death < prey_death_arr)] = np.nan | |
| pred_death = np.random.random(size=grid.shape) | |
| grid_copy[(grid == 2) & (pred_death < pred_death_val)] = 0 | |
| count = np.count_nonzero(grid) | |
| indices = np.random.permutation(count) | |
| rs = indices // cols | |
| cs = indices % cols | |
| nb = np.random.randint(0, n_shifts, size=count) | |
| nrs = (rs + dr_arr[nb]) % rows | |
| ncs = (cs + dc_arr[nb]) % cols | |
| for r, c, nr, nc in zip(rs, cs, nrs, ncs): | |
| state = grid[r, c] | |
| nstate = grid[nr, nc] | |
| if state == 1 and nstate == 0 and np.random.random() < p_birth_val: | |
| grid_copy[nr, nc] = 1 | |
| parent_val = prey_death_arr[r, c] | |
| if not evolution_stopped: | |
| child_val = parent_val + np.random.normal(0, evolve_sd) | |
| prey_death_arr_copy[nr, nc] = np.clip(child_val, evolve_min, evolve_max) | |
| else: | |
| prey_death_arr_copy[nr, nc] = parent_val | |
| elif state == 2 and nstate == 1 and np.random.random() < pred_birth_val: | |
| grid_copy[nr, nc] = 2 | |
| prey_death_arr_copy[nr, nc] = np.nan | |
| grid = grid_copy | |
| prey_death_arr = prey_death_arr_copy | |
| return grid | |
| @njit(cache=True) |
| grid = grid_copy | ||
| prey_death_arr = prey_death_arr_copy |
There was a problem hiding this comment.
These variable assignments are useless because the function returns the original grid, not the modified grid_copy. The function should return both grid_copy and prey_death_arr_copy, or these assignments should be removed. Currently, all modifications to grid_copy and prey_death_arr_copy are discarded.
| grid = grid_copy | |
| prey_death_arr = prey_death_arr_copy | |
| # Copy results back into the original arrays so the caller sees updates. | |
| grid[:, :] = grid_copy | |
| prey_death_arr[:, :] = prey_death_arr_copy |
No description provided.