16164. cache=True for persistent JIT compilation
1717
1818Usage:
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 )
12061def _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"\n Random 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+
938985def 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
9951042if __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"\n Sample stats: largest={ stats ['largest' ]} , "
10031055 f"largest_fraction={ stats ['largest_fraction' ]:.3f} , "
0 commit comments