@@ -338,7 +338,188 @@ def analyze_soc_robustness(experiment_results: List[Dict]) -> Dict:
338338
339339
340340# ============================================================================
341- # 5. VISUALIZATION
341+ # 5. AVALANCHE VISUALIZATION
342+ # ============================================================================
343+
344+ def track_avalanche_propagation (grids_history : List [np .ndarray ],
345+ avalanches : List [Tuple [int , float ]],
346+ sensitivity : float = 0.05 ) -> Dict :
347+ """
348+ Track spatial propagation of avalanches on the grid.
349+
350+ Args:
351+ grids_history: List of grid snapshots
352+ avalanches: List of (time_step, magnitude) tuples
353+ sensitivity: Threshold for detecting change propagation
354+
355+ Returns:
356+ Dict with avalanche events containing spatial information
357+ """
358+ avalanche_events = []
359+ max_pop = max ([(g > 0 ).sum () for g in grids_history ])
360+
361+ for event_t , event_mag in avalanches :
362+ # Look at grids around event time (±5 steps)
363+ window = 5
364+ start_t = max (0 , event_t - window )
365+ end_t = min (len (grids_history ) - 1 , event_t + window )
366+
367+ # Compute change magnitude for each cell
368+ change_map = np .zeros (grids_history [0 ].shape )
369+ if event_t > 0 :
370+ before = (grids_history [event_t - 1 ] > 0 ).astype (float )
371+ after = (grids_history [event_t ] > 0 ).astype (float )
372+ change_map = np .abs (after - before )
373+
374+ # Track population changes
375+ pop_before = (grids_history [start_t ] > 0 ).sum () if start_t < len (grids_history ) else 0
376+ pop_after = (grids_history [min (event_t + 2 , len (grids_history )- 1 )] > 0 ).sum ()
377+ pop_change = abs (pop_after - pop_before )
378+
379+ avalanche_events .append ({
380+ "time" : event_t ,
381+ "magnitude" : event_mag ,
382+ "pop_change" : pop_change ,
383+ "change_map" : change_map ,
384+ "window" : (start_t , end_t )
385+ })
386+
387+ return {"events" : avalanche_events , "max_pop" : max_pop }
388+
389+
390+ def visualize_avalanche_details (experiment_results : List [Dict ],
391+ output_file : Optional [str ] = None ,
392+ redetect_threshold : Optional [float ] = None ):
393+ """
394+ Create detailed avalanche visualization showing:
395+ - Spatial propagation of avalanches
396+ - Avalanche magnitude distribution
397+ - Timeline of avalanche events
398+ - Grid snapshots during/after avalanches
399+
400+ Args:
401+ experiment_results: List of experiment results
402+ output_file: Optional file path to save figure
403+ redetect_threshold: Optional lower threshold to re-detect more avalanches (e.g., 0.02)
404+ If provided, will re-detect avalanches with this threshold
405+ """
406+ fig = plt .figure (figsize = (16 , 12 ))
407+ gs = GridSpec (3 , 3 , figure = fig , hspace = 0.35 , wspace = 0.3 )
408+
409+ # Select representative experiment
410+ rep_idx = len (experiment_results ) // 2
411+ rep_result = experiment_results [rep_idx ]
412+
413+ # Re-detect avalanches with lower threshold if specified
414+ if redetect_threshold is not None :
415+ avalanches = detect_avalanche_events (rep_result ["grids_history" ],
416+ population_change_threshold = redetect_threshold )
417+ else :
418+ avalanches = rep_result ["avalanches" ]
419+
420+ # ========== TOP ROW: AVALANCHE TIMELINE AND SIZE DISTRIBUTION ==========
421+
422+ # Plot 1: Avalanche timeline
423+ ax1 = fig .add_subplot (gs [0 , :2 ])
424+
425+ if avalanches :
426+ times = [t for t , _ in avalanches ]
427+ mags = [mag for _ , mag in avalanches ]
428+
429+ # Color by magnitude with enhanced visibility
430+ scatter = ax1 .scatter (times , mags , c = mags , cmap = 'hot' , s = 350 ,
431+ alpha = 0.8 , edgecolors = 'darkred' , linewidth = 2.5 )
432+
433+ # Add connecting line to show temporal evolution
434+ times_sorted = sorted (range (len (mags )), key = lambda i : times [i ])
435+ sorted_times = [times [i ] for i in times_sorted ]
436+ sorted_mags = [mags [i ] for i in times_sorted ]
437+ ax1 .plot (sorted_times , sorted_mags , 'darkred' , alpha = 0.4 , linewidth = 2 )
438+
439+ cbar = plt .colorbar (scatter , ax = ax1 )
440+ cbar .set_label ('Magnitude (Frac. Change)' , fontsize = 11 , fontweight = 'bold' )
441+
442+ # Add threshold line if re-detected
443+ if redetect_threshold is not None :
444+ ax1 .text (0.02 , 0.98 , f'Detection Threshold: { redetect_threshold :.3f} ' ,
445+ transform = ax1 .transAxes , fontsize = 10 , verticalalignment = 'top' ,
446+ bbox = dict (boxstyle = 'round' , facecolor = 'yellow' , alpha = 0.7 ))
447+ else :
448+ ax1 .text (0.5 , 0.5 , 'No Avalanches Detected' ,
449+ ha = 'center' , va = 'center' , fontsize = 14 , fontweight = 'bold' , color = 'red' )
450+
451+ ax1 .axvline (rep_result ["n_equilibration" ], color = 'red' , linestyle = '--' ,
452+ linewidth = 2.5 , alpha = 0.7 , label = 'Perturbation Start' )
453+ ax1 .set_xlabel ('Time Step' , fontsize = 12 , fontweight = 'bold' )
454+ ax1 .set_ylabel ('Avalanche Magnitude' , fontsize = 12 , fontweight = 'bold' )
455+ ax1 .set_title ('Avalanche Timeline: Temporal Sequence of Events' ,
456+ fontsize = 13 , fontweight = 'bold' , color = 'darkred' )
457+ ax1 .legend (fontsize = 11 , loc = 'upper left' )
458+ ax1 .grid (True , alpha = 0.4 , linewidth = 1.5 )
459+
460+ # Plot 2: Magnitude distribution (histogram)
461+ ax2 = fig .add_subplot (gs [0 , 2 ])
462+
463+ if avalanches :
464+ mags = np .array ([mag for _ , mag in avalanches ])
465+ ax2 .hist (mags , bins = max (8 , len (mags )// 2 ), color = 'orangered' ,
466+ edgecolor = 'darkred' , alpha = 0.8 , linewidth = 2 )
467+ ax2 .set_xlabel ('Magnitude' , fontsize = 11 , fontweight = 'bold' )
468+ ax2 .set_ylabel ('Frequency' , fontsize = 11 , fontweight = 'bold' )
469+ ax2 .set_title (f'Avalanche\n Size Distribution\n (N={ len (mags )} )' ,
470+ fontsize = 12 , fontweight = 'bold' , color = 'darkred' )
471+ ax2 .grid (True , alpha = 0.4 , axis = 'y' , linewidth = 1.5 )
472+
473+ # Add statistics text
474+ stats_mini = f"μ={ mags .mean ():.4f} \n σ={ mags .std ():.4f} "
475+ ax2 .text (0.98 , 0.97 , stats_mini , transform = ax2 .transAxes ,
476+ fontsize = 9 , verticalalignment = 'top' , horizontalalignment = 'right' ,
477+ fontfamily = 'monospace' ,
478+ bbox = dict (boxstyle = 'round' , facecolor = 'lightyellow' , alpha = 0.8 ))
479+ else :
480+ ax2 .text (0.5 , 0.5 , 'No Data' , ha = 'center' , va = 'center' , fontsize = 11 , fontweight = 'bold' )
481+ ax2 .set_title ('Size Distribution' , fontsize = 12 , fontweight = 'bold' )
482+
483+
484+ # ========== MIDDLE ROW: GRID SNAPSHOTS DURING AVALANCHES ==========
485+
486+ # Show grid snapshots at different avalanche times
487+ if avalanches and rep_result ["grids_history" ]:
488+ # Get up to 3 representative avalanche times
489+ avalanche_times = [t for t , _ in avalanches ]
490+ if len (avalanche_times ) > 3 :
491+ selected_times = [avalanche_times [i ] for i in np .linspace (0 , len (avalanche_times )- 1 , 3 ).astype (int )]
492+ else :
493+ selected_times = avalanche_times
494+
495+ for idx , t in enumerate (selected_times ):
496+ ax = fig .add_subplot (gs [1 , idx ])
497+
498+ if 0 <= t < len (rep_result ["grids_history" ]):
499+ grid = rep_result ["grids_history" ][t ]
500+ im = ax .imshow (grid , cmap = 'RdYlGn_r' , interpolation = 'nearest' , vmin = 0 , vmax = 2 )
501+ mag = next ((m for tm , m in avalanches if tm == t ), 0 )
502+ ax .set_title (f'Grid at T={ t } \n (Magnitude: { mag :.4f} )' ,
503+ fontsize = 11 , fontweight = 'bold' , color = 'darkred' )
504+ ax .set_xticks ([])
505+ ax .set_yticks ([])
506+ cbar = plt .colorbar (im , ax = ax , fraction = 0.046 , pad = 0.04 )
507+ cbar .set_ticks ([0 , 1 , 2 ])
508+ cbar .set_ticklabels (['Empty' , 'Prey' , 'Pred' ], fontsize = 9 )
509+
510+
511+ plt .suptitle ('Prey-Predator CA: Detailed Avalanche Analysis' ,
512+ fontsize = 14 , fontweight = 'bold' , y = 0.995 )
513+
514+ if output_file :
515+ plt .savefig (output_file , dpi = 150 , bbox_inches = 'tight' )
516+ print (f"Avalanche visualization saved to { output_file } " )
517+
518+ return fig
519+
520+
521+ # ============================================================================
522+ # 6. MAIN VISUALIZATION
342523# ============================================================================
343524
344525def visualize_soc_properties (experiment_results : List [Dict ],
@@ -528,10 +709,16 @@ def main():
528709 print ()
529710
530711 # Create visualization
531- print ("[4/4] Creating comprehensive visualization ..." )
712+ print ("[4/4] Creating comprehensive visualizations ..." )
532713 output_path = Path (__file__ ).parent .parent / "soc_analysis_results.png"
533714 visualize_soc_properties (experiment_results , robustness_metrics , str (output_path ))
534- print (f" Saved to: { output_path } " )
715+ print (f" Main SOC visualization saved to: { output_path } " )
716+
717+ # Detailed avalanche visualization with lower detection threshold for visibility
718+ avalanche_path = Path (__file__ ).parent .parent / "avalanche_analysis.png"
719+ visualize_avalanche_details (experiment_results , str (avalanche_path ), redetect_threshold = 0.02 )
720+ print (f" Avalanche details saved to: { avalanche_path } " )
721+ print (f" (Using detection threshold: 0.02 for enhanced visibility)" )
535722
536723
537724if __name__ == "__main__" :
0 commit comments