@@ -571,7 +571,6 @@ def run_single_simulation_fss(
571571
572572 sample_counter += 1
573573
574- # <-- FIX: Build result dict AFTER the loop, not inside it
575574 result = {
576575 "prey_birth" : prey_birth ,
577576 "prey_death" : prey_death ,
@@ -814,298 +813,6 @@ def run_debug_mode(cfg: Config, logger: logging.Logger):
814813 logger .info ("Simulation complete." )
815814 input ("Press Enter to exit..." )
816815
817- # =============================================================================
818- # PLOTTING
819- # =============================================================================
820-
821- def generate_plots (cfg : Config , output_dir : Path , logger : logging .Logger ):
822- """Generate all analysis plots from saved data."""
823- import matplotlib .pyplot as plt
824- from collections import defaultdict
825-
826- plt .rcParams ["figure.figsize" ] = (14 , 10 )
827- plt .rcParams ["font.size" ] = 11
828-
829- prey_births = cfg .get_prey_births ()
830- prey_deaths = cfg .get_prey_deaths ()
831- n_pb , n_pd = len (prey_births ), len (prey_deaths )
832- extent = [prey_births [0 ], prey_births [- 1 ], prey_deaths [0 ], prey_deaths [- 1 ]]
833-
834- # Load sweep results
835- sweep_file = output_dir / "sweep_results.npz"
836- if not sweep_file .exists ():
837- # Try JSON fallback
838- sweep_file = output_dir / "sweep_results.json"
839- if not sweep_file .exists ():
840- logger .error (f"Sweep results not found" )
841- return
842- with open (sweep_file , "r" ) as f :
843- results = json .load (f )
844- else :
845- results = load_sweep_binary (sweep_file )
846-
847- logger .info (f"Loaded { len (results )} results" )
848-
849- # Initialize grids
850- grids = {
851- "prey_pop_no_evo" : np .full ((n_pd , n_pb ), np .nan ),
852- "prey_pop_evo" : np .full ((n_pd , n_pb ), np .nan ),
853- "pred_pop_no_evo" : np .full ((n_pd , n_pb ), np .nan ),
854- "pred_pop_evo" : np .full ((n_pd , n_pb ), np .nan ),
855- "survival_prey_no_evo" : np .full ((n_pd , n_pb ), np .nan ),
856- "survival_prey_evo" : np .full ((n_pd , n_pb ), np .nan ),
857- "tau_prey" : np .full ((n_pd , n_pb ), np .nan ),
858- "evolved_prey_death" : np .full ((n_pd , n_pb ), np .nan ),
859- "segregation_index" : np .full ((n_pd , n_pb ), np .nan ),
860- "prey_clustering_index" : np .full ((n_pd , n_pb ), np .nan ),
861- }
862-
863- # Group by parameters
864- grouped = defaultdict (list )
865- for r in results :
866- key = (round (r ["prey_birth" ], 4 ), round (r ["prey_death" ], 4 ), r ["with_evolution" ])
867- grouped [key ].append (r )
868-
869- # Aggregate into grids
870- for i , pd in enumerate (prey_deaths ):
871- for j , pb in enumerate (prey_births ):
872- pd_r , pb_r = round (pd , 4 ), round (pb , 4 )
873-
874- # No evolution
875- no_evo = grouped .get ((pb_r , pd_r , False ), [])
876- if no_evo :
877- grids ["prey_pop_no_evo" ][i , j ] = np .mean ([r ["prey_mean" ] for r in no_evo ])
878- grids ["pred_pop_no_evo" ][i , j ] = np .mean ([r ["pred_mean" ] for r in no_evo ])
879- grids ["survival_prey_no_evo" ][i , j ] = np .mean ([r ["prey_survived" ] for r in no_evo ]) * 100
880-
881- taus = [r ["prey_tau" ] for r in no_evo if not np .isnan (r .get ("prey_tau" , np .nan ))]
882- if taus :
883- grids ["tau_prey" ][i , j ] = np .mean (taus )
884-
885- seg = [r .get ("segregation_index" , np .nan ) for r in no_evo ]
886- seg = [s for s in seg if not np .isnan (s )]
887- if seg :
888- grids ["segregation_index" ][i , j ] = np .mean (seg )
889-
890- clust = [r .get ("prey_clustering_index" , np .nan ) for r in no_evo ]
891- clust = [c for c in clust if not np .isnan (c )]
892- if clust :
893- grids ["prey_clustering_index" ][i , j ] = np .mean (clust )
894-
895- # With evolution
896- evo = grouped .get ((pb_r , pd_r , True ), [])
897- if evo :
898- grids ["prey_pop_evo" ][i , j ] = np .mean ([r ["prey_mean" ] for r in evo ])
899- grids ["pred_pop_evo" ][i , j ] = np .mean ([r ["pred_mean" ] for r in evo ])
900- grids ["survival_prey_evo" ][i , j ] = np .mean ([r ["prey_survived" ] for r in evo ]) * 100
901-
902- evolved = [r .get ("evolved_prey_death_mean" , np .nan ) for r in evo ]
903- evolved = [e for e in evolved if not np .isnan (e )]
904- if evolved :
905- grids ["evolved_prey_death" ][i , j ] = np .mean (evolved )
906-
907- # Compute Hydra derivative
908- dd = prey_deaths [1 ] - prey_deaths [0 ]
909- dN_dd_no_evo = np .zeros_like (grids ["prey_pop_no_evo" ])
910- dN_dd_evo = np .zeros_like (grids ["prey_pop_evo" ])
911-
912- for j in range (n_pb ):
913- pop_smooth = gaussian_filter1d (grids ["prey_pop_no_evo" ][:, j ], sigma = 0.8 )
914- dN_dd_no_evo [:, j ] = np .gradient (pop_smooth , dd )
915- pop_smooth = gaussian_filter1d (grids ["prey_pop_evo" ][:, j ], sigma = 0.8 )
916- dN_dd_evo [:, j ] = np .gradient (pop_smooth , dd )
917-
918- # =========================================================================
919- # PLOT 1: Phase Diagrams
920- # =========================================================================
921- fig , axes = plt .subplots (2 , 3 , figsize = (16 , 10 ))
922-
923- ax = axes [0 , 0 ]
924- im = ax .imshow (grids ["prey_pop_no_evo" ], origin = "lower" , aspect = "auto" ,
925- extent = extent , cmap = "YlGn" )
926- ax .contour (prey_births , prey_deaths , grids ["survival_prey_no_evo" ],
927- levels = [50 ], colors = "black" , linewidths = 2 )
928- plt .colorbar (im , ax = ax , label = "Population" )
929- ax .set_xlabel ("Prey Birth Rate" )
930- ax .set_ylabel ("Prey Death Rate" )
931- ax .set_title ("Prey Pop (No Evolution)" )
932-
933- ax = axes [0 , 1 ]
934- im = ax .imshow (grids ["prey_pop_evo" ], origin = "lower" , aspect = "auto" ,
935- extent = extent , cmap = "YlGn" )
936- ax .contour (prey_births , prey_deaths , grids ["survival_prey_evo" ],
937- levels = [50 ], colors = "black" , linewidths = 2 )
938- plt .colorbar (im , ax = ax , label = "Population" )
939- ax .set_xlabel ("Prey Birth Rate" )
940- ax .set_ylabel ("Prey Death Rate" )
941- ax .set_title ("Prey Pop (With Evolution)" )
942-
943- ax = axes [0 , 2 ]
944- advantage = np .where (
945- grids ["prey_pop_no_evo" ] > 10 ,
946- (grids ["prey_pop_evo" ] - grids ["prey_pop_no_evo" ]) / grids ["prey_pop_no_evo" ] * 100 ,
947- np .where (grids ["prey_pop_evo" ] > 10 , 500 , 0 ),
948- )
949- im = ax .imshow (np .clip (advantage , - 50 , 200 ), origin = "lower" , aspect = "auto" ,
950- extent = extent , cmap = "RdYlGn" , vmin = - 50 , vmax = 200 )
951- plt .colorbar (im , ax = ax , label = "Advantage (%)" )
952- ax .set_xlabel ("Prey Birth Rate" )
953- ax .set_ylabel ("Prey Death Rate" )
954- ax .set_title ("Evolution Advantage" )
955-
956- ax = axes [1 , 0 ]
957- im = ax .imshow (grids ["tau_prey" ], origin = "lower" , aspect = "auto" ,
958- extent = extent , cmap = "coolwarm" , vmin = 1.5 , vmax = 2.5 )
959- ax .contour (prey_births , prey_deaths , grids ["tau_prey" ],
960- levels = [2.05 ], colors = "green" , linewidths = 2 )
961- plt .colorbar (im , ax = ax , label = "τ" )
962- ax .set_xlabel ("Prey Birth Rate" )
963- ax .set_ylabel ("Prey Death Rate" )
964- ax .set_title ("Prey τ (Green: τ=2.05)" )
965-
966- ax = axes [1 , 1 ]
967- im = ax .imshow (grids ["evolved_prey_death" ], origin = "lower" , aspect = "auto" ,
968- extent = extent , cmap = "viridis" )
969- plt .colorbar (im , ax = ax , label = "Evolved d" )
970- ax .set_xlabel ("Prey Birth Rate" )
971- ax .set_ylabel ("Initial Prey Death Rate" )
972- ax .set_title ("Evolved Prey Death Rate" )
973-
974- ax = axes [1 , 2 ]
975- im = ax .imshow (dN_dd_no_evo , origin = "lower" , aspect = "auto" ,
976- extent = extent , cmap = "RdBu_r" , vmin = - 5000 , vmax = 5000 )
977- ax .contour (prey_births , prey_deaths , dN_dd_no_evo ,
978- levels = [0 ], colors = "black" , linewidths = 2 )
979- plt .colorbar (im , ax = ax , label = "dN/dd" )
980- ax .set_xlabel ("Prey Birth Rate" )
981- ax .set_ylabel ("Prey Death Rate" )
982- ax .set_title ("HYDRA: dN/dd (Red: Prey ↑ with mortality)" )
983-
984- plt .tight_layout ()
985- plt .savefig (output_dir / "phase_diagrams.png" , dpi = 150 , bbox_inches = "tight" )
986- plt .close ()
987- logger .info ("Saved phase_diagrams.png" )
988-
989- # =========================================================================
990- # PLOT 2: Hydra Analysis
991- # =========================================================================
992- fig , axes = plt .subplots (1 , 3 , figsize = (15 , 5 ))
993-
994- ax = axes [0 ]
995- im = ax .imshow (dN_dd_no_evo , origin = "lower" , aspect = "auto" ,
996- extent = extent , cmap = "RdBu_r" , vmin = - 5000 , vmax = 5000 )
997- ax .contour (prey_births , prey_deaths , dN_dd_no_evo ,
998- levels = [0 ], colors = "black" , linewidths = 2 )
999- plt .colorbar (im , ax = ax , label = "dN/dd" )
1000- ax .set_xlabel ("Prey Birth Rate" )
1001- ax .set_ylabel ("Prey Death Rate" )
1002- ax .set_title ("Hydra (No Evolution)" )
1003-
1004- ax = axes [1 ]
1005- im = ax .imshow (dN_dd_evo , origin = "lower" , aspect = "auto" ,
1006- extent = extent , cmap = "RdBu_r" , vmin = - 5000 , vmax = 5000 )
1007- ax .contour (prey_births , prey_deaths , dN_dd_evo ,
1008- levels = [0 ], colors = "black" , linewidths = 2 )
1009- plt .colorbar (im , ax = ax , label = "dN/dd" )
1010- ax .set_xlabel ("Prey Birth Rate" )
1011- ax .set_ylabel ("Prey Death Rate" )
1012- ax .set_title ("Hydra (With Evolution)" )
1013-
1014- ax = axes [2 ]
1015- mid_pb_idx = n_pb // 2
1016- target_pb = prey_births [mid_pb_idx ]
1017- no_evo_slice = grids ["prey_pop_no_evo" ][:, mid_pb_idx ]
1018- evo_slice = grids ["prey_pop_evo" ][:, mid_pb_idx ]
1019-
1020- ax .plot (prey_deaths , no_evo_slice , 'b-o' ,
1021- label = f'No Evo (pb={ target_pb :.2f} )' , markersize = 4 )
1022- ax .plot (prey_deaths , evo_slice , 'g-s' ,
1023- label = f'With Evo (pb={ target_pb :.2f} )' , markersize = 4 )
1024-
1025- ax .set_xlabel ("Prey Death Rate" )
1026- ax .set_ylabel ("Prey Population" )
1027- ax .set_title (f"Prey Pop vs Death Rate (pb={ target_pb :.2f} )" ) # Dynamic title
1028- ax .legend ()
1029- ax .grid (True , alpha = 0.3 )
1030-
1031- plt .tight_layout ()
1032- plt .savefig (output_dir / "hydra_analysis.png" , dpi = 150 , bbox_inches = "tight" )
1033- plt .close ()
1034- logger .info ("Saved hydra_analysis.png" )
1035-
1036- # =========================================================================
1037- # PLOT 3: PCF Analysis
1038- # =========================================================================
1039- fig , axes = plt .subplots (1 , 3 , figsize = (15 , 5 ))
1040-
1041- ax = axes [0 ]
1042- im = ax .imshow (grids ["segregation_index" ], origin = "lower" , aspect = "auto" ,
1043- extent = extent , cmap = "RdBu" , vmin = 0.5 , vmax = 1.5 )
1044- ax .contour (prey_births , prey_deaths , grids ["segregation_index" ],
1045- levels = [1.0 ], colors = "black" , linewidths = 2 )
1046- plt .colorbar (im , ax = ax , label = "C_cr" )
1047- ax .set_xlabel ("Prey Birth Rate" )
1048- ax .set_ylabel ("Prey Death Rate" )
1049- ax .set_title ("Segregation Index" )
1050-
1051- ax = axes [1 ]
1052- im = ax .imshow (grids ["prey_clustering_index" ], origin = "lower" , aspect = "auto" ,
1053- extent = extent , cmap = "Greens" , vmin = 1.0 , vmax = 3.0 )
1054- plt .colorbar (im , ax = ax , label = "C_rr" )
1055- ax .set_xlabel ("Prey Birth Rate" )
1056- ax .set_ylabel ("Prey Death Rate" )
1057- ax .set_title ("Prey Clustering" )
1058-
1059- ax = axes [2 ]
1060- im = ax .imshow (grids ["segregation_index" ], origin = "lower" , aspect = "auto" ,
1061- extent = extent , cmap = "RdBu" , vmin = 0.5 , vmax = 1.5 , alpha = 0.8 )
1062- ax .contour (prey_births , prey_deaths , dN_dd_no_evo ,
1063- levels = [0 ], colors = "lime" , linewidths = 3 )
1064- ax .contour (prey_births , prey_deaths , grids ["survival_prey_no_evo" ],
1065- levels = [50 ], colors = "black" , linewidths = 2 , linestyles = '--' )
1066- plt .colorbar (im , ax = ax , label = "C_cr" )
1067- ax .set_xlabel ("Prey Birth Rate" )
1068- ax .set_ylabel ("Prey Death Rate" )
1069- ax .set_title ("Segregation + Hydra Boundary" )
1070-
1071- plt .tight_layout ()
1072- plt .savefig (output_dir / "pcf_analysis.png" , dpi = 150 , bbox_inches = "tight" )
1073- plt .close ()
1074- logger .info ("Saved pcf_analysis.png" )
1075-
1076- # =========================================================================
1077- # Summary Statistics
1078- # =========================================================================
1079- summary = {
1080- "coexistence_no_evo" : int (np .sum ((grids ["survival_prey_no_evo" ] > 80 ))),
1081- "hydra_region_size" : int (np .sum ((dN_dd_no_evo > 0 ) & (grids ["prey_pop_no_evo" ] > 50 ))),
1082- "max_hydra_strength" : float (np .nanmax (dN_dd_no_evo )),
1083- "hydra_region_size_evo" : int (np .sum ((dN_dd_evo > 0 ) & (grids ["prey_pop_evo" ] > 50 ))),
1084- "mean_segregation_index" : float (np .nanmean (grids ["segregation_index" ])),
1085- "mean_prey_clustering" : float (np .nanmean (grids ["prey_clustering_index" ])),
1086- }
1087-
1088- # Find critical point
1089- dist_crit = np .abs (grids ["tau_prey" ] - 2.05 )
1090- if not np .all (np .isnan (dist_crit )):
1091- min_idx = np .unravel_index (np .nanargmin (dist_crit ), dist_crit .shape )
1092- summary ["critical_prey_birth" ] = float (prey_births [min_idx [1 ]])
1093- summary ["critical_prey_death" ] = float (prey_deaths [min_idx [0 ]])
1094- summary ["critical_tau_prey" ] = float (grids ["tau_prey" ][min_idx ])
1095-
1096- with open (output_dir / "summary.json" , "w" ) as f :
1097- json .dump (summary , f , indent = 2 )
1098-
1099- logger .info ("=" * 60 )
1100- logger .info ("ANALYSIS SUMMARY" )
1101- logger .info ("=" * 60 )
1102- logger .info (f"Hydra region: { summary ['hydra_region_size' ]} combinations" )
1103- logger .info (f"Max Hydra strength: { summary ['max_hydra_strength' ]:.1f} " )
1104- logger .info (f"Mean segregation index: { summary ['mean_segregation_index' ]:.3f} " )
1105- if "critical_prey_birth" in summary :
1106- logger .info (f"Critical point: pb={ summary ['critical_prey_birth' ]:.3f} , pd={ summary ['critical_prey_death' ]:.3f} " )
1107-
1108-
1109816# =============================================================================
1110817# MAIN
1111818# =============================================================================
@@ -1182,8 +889,7 @@ def main():
1182889 run_fss (cfg , output_dir , logger )
1183890
1184891 if args .mode in ["full" , "plot" ]:
1185- generate_plots (cfg , output_dir , logger )
1186-
892+ pass #NOTE: Decoupled plots into a separate script for clarity
1187893 elapsed = time .time () - start_time
1188894 logger .info (f"Total runtime: { elapsed / 3600 :.2f} hours" )
1189895 logger .info ("Done!" )
0 commit comments