1919 python pp_analysis.py --mode plot # Only generate plots from saved data
2020 python pp_analysis.py --mode debug # Interactive visualization (local only)
2121 python scripts/pp_analysis.py --dry-run # Estimate runtime without running
22+
23+
24+ Stage 1: Discovery --mode sweep
25+
26+ Stage 2: Targeted FSS
27+ Obtain critical_prey_death and critical_prey_death.
28+ Update the config with target_prey_birth and target_birth_death
29+
30+ Run FSS mode: python pp_analysis.py --mode fss
2231"""
2332
2433import argparse
@@ -127,6 +136,10 @@ def cluster_interval(self) -> int:
127136 # Min density required for PCF/Clsuter Analysis
128137 min_analysis_density : float = 0.002 # FIXME: Minimum prey density (fraction of grid) to analyze clusters/PCF
129138
139+
140+ target_prey_birth : float = 0.22 # FIXME: Change after obtaining results
141+ target_prey_death : float = 0.04 # FIXME; Change after obtaining results
142+
130143 # Parallelization
131144 n_jobs : int = - 1
132145
@@ -399,7 +412,7 @@ def run_single_simulation(
399412 pred_clusters .extend (measure_cluster_sizes_fast (model .grid , 2 ))
400413
401414 # Compute PCFs if enabled for this run
402- if compute_pcf and prey > 20 and pred > 5 :
415+ if compute_pcf :
403416 max_dist = min (grid_size / 2 , cfg .pcf_max_distance )
404417 pcf_data = compute_all_pcfs_fast (model .grid , max_dist , cfg .pcf_n_bins )
405418 pcf_samples ['prey_prey' ].append (pcf_data ['prey_prey' ])
@@ -576,7 +589,7 @@ def run_single_simulation_fss(
576589# =============================================================================
577590
578591def run_2d_sweep (cfg : Config , output_dir : Path , logger : logging .Logger ) -> List [Dict ]:
579- """Run full 2D parameter sweep."""
592+ """Run full 2D parameter sweep with incremental JSONL saving ."""
580593 from joblib import Parallel , delayed
581594
582595 if USE_NUMBA :
@@ -590,52 +603,65 @@ def run_2d_sweep(cfg: Config, output_dir: Path, logger: logging.Logger) -> List[
590603 for pb in prey_births :
591604 for pd in prey_deaths :
592605 for rep in range (cfg .n_replicates ):
606+ # Unique seed for standard run
593607 seed = generate_unique_seed (pb , pd , rep )
594- # Both with and without evolution
595- jobs .append ((pb , pd , cfg .default_grid , seed , False )) #FIXME: Consider cutting non-evo runs
596- jobs .append ((pb , pd , cfg .default_grid , seed , True ))
597-
598- logger .info (f"2D Sweep: { len (jobs ):,} simulations" )
599- logger .info (f" Grid: { len (prey_births )} ×{ len (prey_deaths )} parameters" )
600- logger .info (f" prey_birth: [{ cfg .prey_birth_min :.3f} , { cfg .prey_birth_max :.3f} ]" )
601- logger .info (f" prey_death: [{ cfg .prey_death_min :.3f} , { cfg .prey_death_max :.3f} ]" )
602- logger .info (f" Replicates: { cfg .n_replicates } " )
603- logger .info (f" PCF sample rate: { cfg .pcf_sample_rate :.0%} " )
604-
605- results = Parallel (n_jobs = cfg .n_jobs , verbose = 0 )(
606- delayed (run_single_simulation )(pb , pd , gs , seed , evo , cfg )
607- for pb , pd , gs , seed , evo in tqdm (jobs , desc = "2D Sweep Progress" , mininterval = 30 )
608- )
608+ jobs .append ((pb , pd , cfg .default_grid , seed , False ))
609+
610+ # Different unique seed for evolutionary run
611+ evo_seed = generate_unique_seed (pb , pd , rep + 1000000 )
612+ jobs .append ((pb , pd , cfg .default_grid , evo_seed , True ))
609613
610- # Save results
611- output_file = output_dir / "sweep_results.npz"
612- save_sweep_binary (results , output_file )
614+ output_jsonl = output_dir / "sweep_results.jsonl"
615+ logger .info (f"Starting sweep: { len (jobs ):,} simulations" )
616+ logger .info (f"Incremental results will be saved to { output_jsonl } " )
617+
618+ all_results = []
619+
620+ # Using 'return_as="generator"' allows us to save as each job finishes
621+ # This prevents data loss if the 72-hour limit is reached early
622+ with open (output_jsonl , "a" , encoding = "utf-8" ) as f :
623+ # Create the parallel executor
624+ executor = Parallel (n_jobs = cfg .n_jobs , return_as = "generator" )
625+ tasks = (delayed (run_single_simulation )(pb , pd , gs , seed , evo , cfg )
626+ for pb , pd , gs , seed , evo in jobs )
627+
628+ # Iterate through completed results
629+ for result in tqdm (executor (tasks ), total = len (jobs ), desc = "2D Sweep Progress" ):
630+ # 1. Save to JSONL immediately (Safety)
631+ f .write (json .dumps (result ) + "\n " )
632+ f .flush () # Force write to disk
633+
634+ # 2. Store in memory for return/binary save (Optimization)
635+ all_results .append (result )
636+
637+ output_npz = output_dir / "sweep_results.npz"
638+ save_sweep_binary (all_results , output_npz )
613639
614640 meta = {
615- "n_sims" : len (results ),
641+ "n_sims" : len (all_results ),
616642 "timestamp" : time .strftime ("%Y-%m-%d %H:%M:%S" ),
617643 "grid_size" : cfg .default_grid ,
618644 "pcf_sample_rate" : cfg .pcf_sample_rate ,
619645 }
620646 with open (output_dir / "sweep_metadata.json" , "w" ) as f :
621647 json .dump (meta , f , indent = 2 )
622648
623- logger .info (f"Saved sweep results to { output_file } " )
624- return results
649+ logger .info (f"Sweep complete. Binary data saved to { output_npz } " )
650+ return all_results
625651
626652
627653def run_sensitivity (cfg : Config , output_dir : Path , logger : logging .Logger ) -> List [Dict ]:
628654 """Run evolution parameter sensitivity analysis."""
629655 from joblib import Parallel , delayed
630656
631657 # Fixed parameters in transition zone
632- pb_test = 0.20
633- pd_test = 0.05
658+ pb_test = cfg . target_prey_birth
659+ pd_test = cfg . target_prey_death
634660
635661 jobs = []
636662 for sd in cfg .sensitivity_sd_values :
637663 for rep in range (cfg .sensitivity_replicates ):
638- seed = int ( sd * 100000 ) + rep
664+ seed = generate_unique_seed ( pb_test , pd_test , rep + 2000000 )
639665 jobs .append ((pb_test , pd_test , cfg .default_grid , seed , True , sd ))
640666
641667 logger .info (f"Sensitivity: { len (jobs )} simulations" )
@@ -659,8 +685,8 @@ def run_fss(cfg: Config, output_dir: Path, logger: logging.Logger) -> List[Dict]
659685 from joblib import Parallel , delayed
660686
661687 # Fixed parameters near critical point
662- pb_test = 0.20
663- pd_test = 0.03
688+ pb_test = cfg . target_prey_birth
689+ pd_test = cfg . target_prey_death
664690
665691 # Validation
666692 logger .info ("=" * 60 )
@@ -690,7 +716,7 @@ def run_fss(cfg: Config, output_dir: Path, logger: logging.Logger) -> List[Dict]
690716 measurement_steps = int (cfg .measurement_steps * warmup_factor )
691717
692718 for rep in range (cfg .fss_replicates ):
693- seed = L * 1000 + rep
719+ seed = generate_unique_seed ( pb_test , pd_test , rep + 2000000 )
694720 jobs .append ((pb_test , pd_test , L , seed , warmup_steps , measurement_steps ))
695721
696722 logger .info (f"FSS: { len (jobs )} simulations" )
0 commit comments