Skip to content

Commit efd6f38

Browse files
committed
fixed kernel logic
1 parent 129e3d7 commit efd6f38

1 file changed

Lines changed: 139 additions & 87 deletions

File tree

models/numba_optimized.py

Lines changed: 139 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
4. cache=True for persistent JIT compilation
1717
1818
Usage:
19-
from numba_optimized_enhanced import (
19+
from numba_optimized import (
2020
PPKernel,
2121
compute_all_pcfs_fast,
2222
measure_cluster_sizes_fast, # Sizes only (fastest)
@@ -56,65 +56,6 @@ def set_numba_seed(seed: int) -> None:
5656
# ============================================================================
5757
# PREDATOR-PREY KERNELS
5858
# ============================================================================
59-
@njit(cache=True)
60-
def _pp_async_kernel_fast(
61-
grid: np.ndarray,
62-
prey_death_arr: np.ndarray,
63-
p_birth_val: float,
64-
p_death_val: float,
65-
pred_birth_val: float,
66-
pred_death_val: float,
67-
dr_arr: np.ndarray,
68-
dc_arr: np.ndarray,
69-
evolve_sd: float,
70-
evolve_min: float,
71-
evolve_max: float,
72-
evolution_stopped: bool,
73-
occupied_buffer: np.ndarray,
74-
) -> np.ndarray:
75-
"""Partially synchronous predator-prey update kernel."""
76-
rows, cols = grid.shape
77-
n_shifts = len(dr_arr)
78-
grid_copy = grid.copy()
79-
prey_death_arr_copy = prey_death_arr.copy()
80-
81-
prey_death = np.random.random(size=grid.shape)
82-
grid_copy[(grid == 1) & (prey_death < prey_death_arr)] = 0
83-
prey_death_arr_copy[(grid == 1) & (prey_death < prey_death_arr)] = np.nan
84-
85-
pred_death = np.random.random(size=grid.shape)
86-
grid_copy[(grid == 2) & (pred_death < pred_death_val)] = 0
87-
88-
count = np.count_nonzero(grid)
89-
indices = np.random.permutation(count)
90-
rs = indices // cols
91-
cs = indices % cols
92-
93-
nb = np.random.randint(0, n_shifts, size=count)
94-
nrs = (rs + dr_arr[nb]) % rows
95-
ncs = (cs + dc_arr[nb]) % cols
96-
97-
for r, c, nr, nc in zip(rs, cs, nrs, ncs):
98-
state = grid[r, c]
99-
nstate = grid[nr, nc]
100-
101-
if state == 1 and nstate == 0 and np.random.random() < p_birth_val:
102-
grid_copy[nr, nc] = 1
103-
parent_val = prey_death_arr[r, c]
104-
if not evolution_stopped:
105-
child_val = parent_val + np.random.normal(0, evolve_sd)
106-
prey_death_arr_copy[nr, nc] = np.clip(child_val, evolve_min, evolve_max)
107-
else:
108-
prey_death_arr_copy[nr, nc] = parent_val
109-
110-
elif state == 2 and nstate == 1 and np.random.random() < pred_birth_val:
111-
grid_copy[nr, nc] = 2
112-
prey_death_arr_copy[nr, nc] = np.nan
113-
114-
grid = grid_copy
115-
prey_death_arr = prey_death_arr_copy
116-
117-
return grid
11859

11960
@njit(cache=True)
12061
def _pp_async_kernel_random(
@@ -132,10 +73,11 @@ def _pp_async_kernel_random(
13273
evolution_stopped: bool,
13374
occupied_buffer: np.ndarray,
13475
) -> np.ndarray:
135-
"""Asynchronous predator-prey update kernel."""
76+
"""Asynchronous predator-prey update kernel with random neighbor selection."""
13677
rows, cols = grid.shape
13778
n_shifts = len(dr_arr)
13879

80+
# Collect occupied cells
13981
count = 0
14082
for r in range(rows):
14183
for c in range(cols):
@@ -150,6 +92,7 @@ def _pp_async_kernel_random(
15092
occupied_buffer[i, 0], occupied_buffer[j, 0] = occupied_buffer[j, 0], occupied_buffer[i, 0]
15193
occupied_buffer[i, 1], occupied_buffer[j, 1] = occupied_buffer[j, 1], occupied_buffer[i, 1]
15294

95+
# Process each occupied cell
15396
for i in range(count):
15497
r = occupied_buffer[i, 0]
15598
c = occupied_buffer[i, 1]
@@ -158,6 +101,7 @@ def _pp_async_kernel_random(
158101
if state == 0:
159102
continue
160103

104+
# Random neighbor selection
161105
nbi = np.random.randint(0, n_shifts)
162106
nr = (r + dr_arr[nbi]) % rows
163107
nc = (c + dc_arr[nbi]) % cols
@@ -207,10 +151,20 @@ def _pp_async_kernel_directed(
207151
evolution_stopped: bool,
208152
occupied_buffer: np.ndarray,
209153
) -> np.ndarray:
210-
"""Async predator-prey update kernel with directed reproduction."""
154+
"""
155+
Asynchronous predator-prey update kernel with directed behavior.
156+
157+
Directed behavior:
158+
- Prey: Searches all neighbors for empty cells, randomly picks one to reproduce into
159+
- Predator: Searches all neighbors for prey, randomly picks one to hunt
160+
161+
This makes both species more "intelligent" compared to random neighbor selection.
162+
Uses efficient two-pass counting approach (Numba-compatible, no heap allocation).
163+
"""
211164
rows, cols = grid.shape
212165
n_shifts = len(dr_arr)
213166

167+
# Collect occupied cells
214168
count = 0
215169
for r in range(rows):
216170
for c in range(cols):
@@ -219,32 +173,53 @@ def _pp_async_kernel_directed(
219173
occupied_buffer[count, 1] = c
220174
count += 1
221175

176+
# Fisher-Yates shuffle
222177
for i in range(count - 1, 0, -1):
223178
j = np.random.randint(0, i + 1)
224179
occupied_buffer[i, 0], occupied_buffer[j, 0] = occupied_buffer[j, 0], occupied_buffer[i, 0]
225180
occupied_buffer[i, 1], occupied_buffer[j, 1] = occupied_buffer[j, 1], occupied_buffer[i, 1]
226181

182+
# Process each occupied cell
227183
for i in range(count):
228184
r = occupied_buffer[i, 0]
229185
c = occupied_buffer[i, 1]
230186

231187
state = grid[r, c]
188+
if state == 0:
189+
continue
232190

233-
if state == 1: # PREY
191+
if state == 1: # PREY - directed reproduction into empty cells
192+
# Check for death first
234193
if np.random.random() < prey_death_arr[r, c]:
235194
grid[r, c] = 0
236195
prey_death_arr[r, c] = np.nan
237-
elif np.random.random() < p_birth_val:
238-
valid = []
196+
continue
197+
198+
# Attempt reproduction with directed selection
199+
if np.random.random() < p_birth_val:
200+
# Pass 1: Count empty neighbors
201+
empty_count = 0
239202
for k in range(n_shifts):
240-
nr = (r + dr_arr[k]) % rows
241-
nc = (c + dc_arr[k]) % cols
242-
if grid[nr, nc] == 0:
243-
valid.append(k)
244-
if len(valid) > 0:
245-
choice = np.random.choice(valid)
246-
nr = (r + dr_arr[choice]) % rows
247-
nc = (c + dc_arr[choice]) % cols
203+
check_r = (r + dr_arr[k]) % rows
204+
check_c = (c + dc_arr[k]) % cols
205+
if grid[check_r, check_c] == 0:
206+
empty_count += 1
207+
208+
# Pass 2: Select random empty neighbor
209+
if empty_count > 0:
210+
target_idx = np.random.randint(0, empty_count)
211+
found = 0
212+
nr, nc = r, c # Initialize (will be overwritten)
213+
for k in range(n_shifts):
214+
check_r = (r + dr_arr[k]) % rows
215+
check_c = (c + dc_arr[k]) % cols
216+
if grid[check_r, check_c] == 0:
217+
if found == target_idx:
218+
nr, nc = check_r, check_c
219+
break
220+
found += 1
221+
222+
# Reproduce into selected empty cell
248223
grid[nr, nc] = 1
249224
parent_val = prey_death_arr[r, c]
250225
if not evolution_stopped:
@@ -257,20 +232,37 @@ def _pp_async_kernel_directed(
257232
else:
258233
prey_death_arr[nr, nc] = parent_val
259234

260-
elif state == 2: # PREDATOR
235+
elif state == 2: # PREDATOR - directed hunting
236+
# Check for death first
261237
if np.random.random() < pred_death_val:
262238
grid[r, c] = 0
263-
elif np.random.random() < pred_birth_val:
264-
valid = []
239+
continue
240+
241+
# Attempt hunting with directed selection
242+
if np.random.random() < pred_birth_val:
243+
# Pass 1: Count prey neighbors
244+
prey_count = 0
265245
for k in range(n_shifts):
266-
nr = (r + dr_arr[k]) % rows
267-
nc = (c + dc_arr[k]) % cols
268-
if grid[nr, nc] == 1:
269-
valid.append(k)
270-
if len(valid) > 0:
271-
choice = np.random.choice(valid)
272-
nr = (r + dr_arr[choice]) % rows
273-
nc = (c + dc_arr[choice]) % cols
246+
check_r = (r + dr_arr[k]) % rows
247+
check_c = (c + dc_arr[k]) % cols
248+
if grid[check_r, check_c] == 1:
249+
prey_count += 1
250+
251+
# Pass 2: Select random prey neighbor
252+
if prey_count > 0:
253+
target_idx = np.random.randint(0, prey_count)
254+
found = 0
255+
nr, nc = r, c # Initialize (will be overwritten)
256+
for k in range(n_shifts):
257+
check_r = (r + dr_arr[k]) % rows
258+
check_c = (c + dc_arr[k]) % cols
259+
if grid[check_r, check_c] == 1:
260+
if found == target_idx:
261+
nr, nc = check_r, check_c
262+
break
263+
found += 1
264+
265+
# Hunt: prey cell becomes predator
274266
grid[nr, nc] = 2
275267
prey_death_arr[nr, nc] = np.nan
276268

@@ -290,7 +282,7 @@ def __init__(self, rows: int, cols: int, neighborhood: str = "moore",
290282
if neighborhood == "moore":
291283
self._dr = np.array([-1, -1, -1, 0, 0, 1, 1, 1], dtype=np.int32)
292284
self._dc = np.array([-1, 0, 1, -1, 1, -1, 0, 1], dtype=np.int32)
293-
else:
285+
else: # von Neumann
294286
self._dr = np.array([-1, 1, 0, 0], dtype=np.int32)
295287
self._dc = np.array([0, 0, -1, 1], dtype=np.int32)
296288

@@ -921,20 +913,75 @@ def warmup_numba_kernels(grid_size: int = 100, directed_hunting: bool = False):
921913
prey_death_arr = np.full((grid_size, grid_size), 0.05, dtype=np.float64)
922914
prey_death_arr[grid != 1] = np.nan
923915

916+
# Always warmup random kernel
924917
kernel_random = PPKernel(grid_size, grid_size, directed_hunting=False)
925918
kernel_random.update(grid.copy(), prey_death_arr.copy(), 0.2, 0.05, 0.2, 0.1)
926919

920+
# Warmup directed kernel if requested
927921
if directed_hunting:
928922
kernel_directed = PPKernel(grid_size, grid_size, directed_hunting=True)
929923
kernel_directed.update(grid.copy(), prey_death_arr.copy(), 0.2, 0.05, 0.2, 0.1)
930924

925+
# Warmup analysis functions
931926
_ = compute_all_pcfs_fast(grid, max_distance=20.0, n_bins=20)
932927
_ = measure_cluster_sizes_fast(grid, 1)
933928
_ = detect_clusters_fast(grid, 1)
934929
_ = get_cluster_stats_fast(grid, 1)
935930
_ = get_percolating_cluster_fast(grid, 1)
936931

937932

933+
def benchmark_kernels(grid_size: int = 100, n_runs: int = 20):
934+
"""Benchmark random vs directed kernels."""
935+
import time
936+
937+
print("=" * 60)
938+
print(f"KERNEL BENCHMARK ({grid_size}x{grid_size}, {n_runs} runs)")
939+
print(f"Numba available: {NUMBA_AVAILABLE}")
940+
print("=" * 60)
941+
942+
np.random.seed(42)
943+
grid = np.zeros((grid_size, grid_size), dtype=np.int32)
944+
n_prey = int(grid_size * grid_size * 0.30)
945+
n_pred = int(grid_size * grid_size * 0.15)
946+
positions = np.random.permutation(grid_size * grid_size)
947+
for pos in positions[:n_prey]:
948+
grid[pos // grid_size, pos % grid_size] = 1
949+
for pos in positions[n_prey:n_prey + n_pred]:
950+
grid[pos // grid_size, pos % grid_size] = 2
951+
952+
prey_death_arr = np.full((grid_size, grid_size), 0.05, dtype=np.float64)
953+
prey_death_arr[grid != 1] = np.nan
954+
955+
print(f"Initial: {np.sum(grid == 1)} prey, {np.sum(grid == 2)} predators")
956+
957+
# Warmup both kernels
958+
warmup_numba_kernels(grid_size, directed_hunting=True)
959+
960+
# Benchmark random kernel
961+
kernel_random = PPKernel(grid_size, grid_size, directed_hunting=False)
962+
t0 = time.perf_counter()
963+
for _ in range(n_runs):
964+
test_grid = grid.copy()
965+
test_arr = prey_death_arr.copy()
966+
kernel_random.update(test_grid, test_arr, 0.2, 0.05, 0.2, 0.1)
967+
t_random = (time.perf_counter() - t0) / n_runs * 1000
968+
969+
# Benchmark directed kernel
970+
kernel_directed = PPKernel(grid_size, grid_size, directed_hunting=True)
971+
t0 = time.perf_counter()
972+
for _ in range(n_runs):
973+
test_grid = grid.copy()
974+
test_arr = prey_death_arr.copy()
975+
kernel_directed.update(test_grid, test_arr, 0.2, 0.05, 0.2, 0.1)
976+
t_directed = (time.perf_counter() - t0) / n_runs * 1000
977+
978+
print(f"\nRandom kernel: {t_random:.2f} ms/step")
979+
print(f"Directed kernel: {t_directed:.2f} ms/step")
980+
print(f"Overhead: {t_directed - t_random:.2f} ms (+{100*(t_directed/t_random - 1):.1f}%)")
981+
982+
return t_random, t_directed
983+
984+
938985
def benchmark_cluster_detection(grid_size: int = 100, n_runs: int = 20):
939986
"""Benchmark cluster detection methods."""
940987
import time
@@ -994,10 +1041,15 @@ def benchmark_cluster_detection(grid_size: int = 100, n_runs: int = 20):
9941041

9951042
if __name__ == "__main__":
9961043
print("\n" + "=" * 60)
997-
print("ENHANCED NUMBA MODULE BENCHMARKS")
1044+
print("NUMBA-OPTIMIZED PP MODULE - BENCHMARKS")
9981045
print("=" * 60 + "\n")
9991046

1000-
warmup_numba_kernels()
1047+
# Run kernel benchmarks
1048+
benchmark_kernels(100)
1049+
1050+
print("\n")
1051+
1052+
# Run cluster benchmarks
10011053
stats = benchmark_cluster_detection(100)
10021054
print(f"\nSample stats: largest={stats['largest']}, "
10031055
f"largest_fraction={stats['largest_fraction']:.3f}, "

0 commit comments

Comments
 (0)