Skip to content

Commit 1d42dbe

Browse files
committed
finally speedup
1 parent f2e3b8d commit 1d42dbe

7 files changed

Lines changed: 126 additions & 30 deletions

File tree

results_stress/sweep_metadata.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"n_sims": 24,
3+
"timestamp": "2026-01-21 19:39:59",
4+
"grid_size": 200
5+
}

results_stress/sweep_results.npz

168 KB
Binary file not shown.

scripts/__jnit__.py

Whitespace-only changes.

scripts/macro_benchmark.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ def run_stress_benchmark():
1515
cfg = Config()
1616
# INCREASE WORKLOAD: 150x150 grid and 1000 total steps
1717
# This ensures the 'race' is long enough to overcome the 'warmup'
18-
cfg.default_grid = 150
18+
cfg.default_grid = 200
1919
cfg.n_prey_birth = 2 # 2x2 sweep is enough to see scaling
2020
cfg.n_prey_death = 2
2121
cfg.n_replicates = 3
2222
cfg.warmup_steps = 400
23-
cfg.measurement_steps = 600
23+
cfg.measurement_steps = 10000
2424
cfg.n_jobs = 4
2525

2626
output_dir = Path("results_stress")

scripts/numba_optimized.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,57 +32,74 @@ def _pp_async_kernel(
3232
rows, cols = grid.shape
3333
n_shifts = len(dr_arr)
3434

35-
# 1. Identify occupied cells
36-
occupied = []
35+
# 1. PRE-ALLOCATE COORDINATE BUFFER
36+
# Instead of a list, we use a fixed-size array.
37+
# Max possible occupied cells is rows * cols.
38+
occupied = np.empty((rows * cols, 2), dtype=np.int32)
39+
count = 0
40+
3741
for r in range(rows):
3842
for c in range(cols):
3943
if grid[r, c] != 0:
40-
occupied.append((r, c))
44+
occupied[count, 0] = r
45+
occupied[count, 1] = c
46+
count += 1
4147

42-
# 2. Shuffle (Asynchronous behavior)
43-
N = len(occupied)
44-
for i in range(N - 1, 0, -1):
48+
# 2. IN-PLACE FISHER-YATES SHUFFLE
49+
# We only shuffle the part of the buffer we actually filled (up to 'count')
50+
for i in range(count - 1, 0, -1):
4551
j = np.random.randint(0, i + 1)
46-
temp = occupied[i]
47-
occupied[i] = occupied[j]
48-
occupied[j] = temp
52+
# Swap row index
53+
r_temp = occupied[i, 0]
54+
occupied[i, 0] = occupied[j, 0]
55+
occupied[j, 0] = r_temp
56+
# Swap col index
57+
c_temp = occupied[i, 1]
58+
occupied[i, 1] = occupied[j, 1]
59+
occupied[j, 1] = c_temp
60+
61+
# 3. PROCESS ACTIVE CELLS
62+
for i in range(count):
63+
r = occupied[i, 0]
64+
c = occupied[i, 1]
4965

50-
# 3. Process
51-
for pos in occupied:
52-
r, c = pos
5366
state = grid[r, c]
54-
if state == 0: continue
67+
if state == 0:
68+
continue
5569

70+
# Pick random neighbor
5671
nbi = np.random.randint(0, n_shifts)
5772
nr = (r + dr_arr[nbi]) % rows
5873
nc = (c + dc_arr[nbi]) % cols
5974

6075
if state == 1: # PREY
61-
# Death
76+
# Death logic
6277
if np.random.random() < prey_death_arr[r, c]:
6378
grid[r, c] = 0
6479
prey_death_arr[r, c] = np.nan
65-
# Birth into empty cell
80+
# Birth logic
6681
elif grid[nr, nc] == 0:
6782
if np.random.random() < p_birth_val:
6883
grid[nr, nc] = 1
69-
# Inheritance
7084
parent_val = prey_death_arr[r, c]
7185
if not evolution_stopped:
7286
child_val = parent_val + np.random.normal(0, evolve_sd)
73-
prey_death_arr[nr, nc] = max(evolve_min, min(evolve_max, child_val))
87+
# Manual clip is faster than np.clip in Numba
88+
if child_val < evolve_min: child_val = evolve_min
89+
if child_val > evolve_max: child_val = evolve_max
90+
prey_death_arr[nr, nc] = child_val
7491
else:
7592
prey_death_arr[nr, nc] = parent_val
7693

7794
elif state == 2: # PREDATOR
78-
# Death
95+
# Death logic
7996
if np.random.random() < pred_death_val:
8097
grid[r, c] = 0
81-
# Birth into prey cell
98+
# Birth logic (eating prey)
8299
elif grid[nr, nc] == 1:
83100
if np.random.random() < pred_birth_val:
84101
grid[nr, nc] = 2
85-
prey_death_arr[nr, nc] = np.nan # Predator eats prey, clear params
102+
prey_death_arr[nr, nc] = np.nan
86103

87104
return grid
88105

scripts/pp_analysis.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
from typing import Dict, List, Tuple, Optional
3434
import warnings
3535

36+
project_root = str(Path(__file__).parents[1])
37+
if project_root not in sys.path:
38+
sys.path.insert(0, project_root)
39+
3640
try:
3741
from scripts.numba_optimized import (
3842
compute_pcf_periodic_fast,
@@ -80,8 +84,8 @@ class Config:
8084
# Simulation length
8185
warmup_steps: int = 200
8286
measurement_steps: int = 300
83-
cluster_samples: int = 10
84-
cluster_interval: int = 100
87+
cluster_samples: int = 3
88+
cluster_interval: int = 1000
8589

8690
# Ecological parameters
8791
evolve_sd: float = 0.10
@@ -104,7 +108,7 @@ class Config:
104108
synchronous: bool = False
105109

106110
# Snapshot settings
107-
save_snapshots: bool = True
111+
save_snapshots: bool = False
108112
snapshot_times: Tuple[int, ...] = (50, 100, 150, 200, 250)
109113

110114
n_jobs: int = -1 # -1 = all available cores
@@ -209,6 +213,7 @@ def get_evolution_metadata(model)->Dict:
209213
return metadata
210214

211215

216+
212217
def collect_comprehensive_metrics(model, step: int) -> Dict:
213218
"""Collect comprehensive metrics using built-in CA methods."""
214219
metrics = {
@@ -581,6 +586,39 @@ def save_diagnostic_snapshot(
581586
plt.savefig(output_path / f'diagnostic_{run_id}.png', dpi=120, bbox_inches='tight')
582587
plt.close()
583588

589+
590+
def save_sweep_binary(results: List[Dict], output_path: Path):
591+
"""Saves high-volume sweep data to compressed .npz to avoid JSON overhead."""
592+
# We use a dictionary to map each run to its own key-space
593+
data_to_save = {}
594+
for i, res in enumerate(results):
595+
prefix = f"run_{i}_"
596+
for key, val in res.items():
597+
# np.savez handles numbers and arrays perfectly;
598+
# objects/dicts are converted to array-wrappers
599+
data_to_save[f"{prefix}{key}"] = np.array(val)
600+
601+
np.savez_compressed(output_path, **data_to_save)
602+
603+
def warmup_numba_kernels(grid_size: int):
604+
"""Compiles kernels in the main thread so workers load from cache instantly."""
605+
logging.info(f"Warming up Numba kernels for {grid_size}x{grid_size} grid...")
606+
607+
# Create dummy data structures matching real types
608+
dummy_grid = np.zeros((grid_size, grid_size), dtype=np.int32)
609+
p_death_arr = np.full((grid_size, grid_size), 0.05, dtype=np.float64)
610+
dr = np.array([-1, 1, 0, 0], dtype=np.int32)
611+
dc = np.array([0, 0, -1, 1], dtype=np.int32)
612+
613+
# Trigger Simulation JIT
614+
from scripts.numba_optimized import _pp_async_kernel, _compute_distance_histogram
615+
_ = _pp_async_kernel(dummy_grid, p_death_arr, 0.2, 0.05, 0.2, 0.1, dr, dc, 0.1, 0.001, 0.1, False)
616+
617+
# Trigger PCF JIT
618+
pos = np.ascontiguousarray(np.argwhere(dummy_grid == 0)[:10], dtype=np.float64)
619+
if len(pos) > 0:
620+
_ = _compute_distance_histogram(pos, pos, float(grid_size), float(grid_size), 20.0, 20, True)
621+
584622

585623
##########################################################################
586624
# Main Simulation Function
@@ -943,6 +981,8 @@ def run_single_simulation_fss(
943981
def run_2d_sweep(cfg, output_dir, logger) -> List[Dict]:
944982
"""Run full 2D parameter sweep with optional diagnostic snapshots."""
945983
from joblib import Parallel, delayed
984+
if USE_NUMBA:
985+
warmup_numba_kernels(cfg.default_grid)
946986

947987
prey_births = cfg.get_prey_births()
948988
prey_deaths = cfg.get_prey_deaths()
@@ -987,14 +1027,22 @@ def run_2d_sweep(cfg, output_dir, logger) -> List[Dict]:
9871027
for pb, pd, gs, seed, evo, save_diag in jobs
9881028
)
9891029

990-
output_file = output_dir / "sweep_results.json"
991-
with open(output_file, "w") as f:
992-
json.dump(results, f)
993-
logger.info(f"Saved to {output_file}")
1030+
# REPLACE THE JSON BLOCK WITH THIS:
1031+
output_file = output_dir / "sweep_results.npz"
1032+
save_sweep_binary(results, output_file)
1033+
1034+
# Save a tiny 'metadata' JSON just for quick human reading
1035+
meta = {
1036+
"n_sims": len(results),
1037+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
1038+
"grid_size": cfg.default_grid
1039+
}
1040+
with open(output_dir / "sweep_metadata.json", "w") as f:
1041+
json.dump(meta, f, indent=2)
9941042

1043+
logger.info(f"SUCCESS: Saved binary sweep results to {output_file}")
9951044
return results
9961045

997-
9981046
def run_sensitivity(
9991047
cfg: Config, output_dir: Path, logger: logging.Logger
10001048
) -> List[Dict]:

scripts/profile_sim.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import cProfile, pstats
2+
from pathlib import Path
3+
import sys
4+
5+
# Ensure we can find our modules
6+
sys.path.insert(0, str(Path(__file__).parent.parent))
7+
from pp_analysis import Config, run_single_simulation
8+
9+
# 1. Setup a single simulation configuration
10+
cfg = Config()
11+
cfg.default_grid = 150
12+
cfg.warmup_steps = 200
13+
cfg.measurement_steps = 300
14+
15+
# 2. Profile the function
16+
profiler = cProfile.Profile()
17+
profiler.enable()
18+
19+
# Run a single simulation (no parallelization)
20+
run_single_simulation(0.2, 0.05, 150, 42, True, cfg)
21+
22+
profiler.disable()
23+
24+
# 3. Print the top 15 time-consumers
25+
stats = pstats.Stats(profiler).sort_stats('tottime')
26+
stats.print_stats(15)

0 commit comments

Comments
 (0)