Skip to content

Commit e40bf12

Browse files
committed
Avalanche visualization
1 parent 518d444 commit e40bf12

3 files changed

Lines changed: 190 additions & 3 deletions

File tree

avalanche_analysis.png

195 KB
Loading

scripts/soc_analysis.py

Lines changed: 190 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nSize 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

344525
def 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

537724
if __name__ == "__main__":

soc_analysis_results.png

227 KB
Loading

0 commit comments

Comments
 (0)