|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Visualize MAP-Elites cell usage from log file.""" |
| 3 | + |
| 4 | +import sys |
| 5 | +import re |
| 6 | +import yaml |
| 7 | +from pathlib import Path |
| 8 | +from collections import defaultdict |
| 9 | +import numpy as np |
| 10 | +import matplotlib.pyplot as plt |
| 11 | +import seaborn as sns |
| 12 | + |
| 13 | +# Parse command line arguments |
| 14 | +# Usage: visualize_cell_usage.py <folder> [config_file] |
| 15 | +folder = Path(sys.argv[1] if len(sys.argv) > 1 else input("Folder: ").strip()) |
| 16 | +config_file = sys.argv[2] if len(sys.argv) > 2 else None |
| 17 | + |
| 18 | +# Find config and log |
| 19 | +if config_file: |
| 20 | + config_path = folder / config_file |
| 21 | + if not config_path.exists(): |
| 22 | + print(f"Error: Specified config file not found: {config_path}") |
| 23 | + exit(1) |
| 24 | +else: |
| 25 | + # Try default config first, then fall back to any config*.yaml |
| 26 | + default_config = folder / "config_adaptive_exploration_full.yaml" |
| 27 | + if default_config.exists(): |
| 28 | + config_path = default_config |
| 29 | + else: |
| 30 | + config_path = next(folder.glob("config*.yaml"), None) |
| 31 | + |
| 32 | +log_path = max((folder / "openevolve_output" / "logs").glob("openevolve_*.log"), key=lambda p: p.stat().st_mtime, default=None) |
| 33 | + |
| 34 | +if not config_path or not log_path: |
| 35 | + print(f"Error: Config or log not found in {folder}") |
| 36 | + if not config_path: |
| 37 | + print(f" Config path: {config_path}") |
| 38 | + if not log_path: |
| 39 | + print(f" Log path: {log_path}") |
| 40 | + exit(1) |
| 41 | + |
| 42 | +# Load bins and num_islands from config |
| 43 | +with open(config_path) as f: |
| 44 | + config_data = yaml.safe_load(f) |
| 45 | + bins = config_data['database']['feature_bins'] |
| 46 | + num_islands = config_data['database'].get('num_islands', 1) |
| 47 | + |
| 48 | +print(f"Using config: {config_path}") |
| 49 | +print(f"Number of islands: {num_islands}") |
| 50 | +print(f"Feature bins: {bins}") |
| 51 | + |
| 52 | +# Parse log - track data per island |
| 53 | +# Data structures: island_id -> defaultdict/dict |
| 54 | +island_usage = {i: defaultdict(int) for i in range(num_islands)} # All events per island |
| 55 | +island_improved = {i: defaultdict(int) for i in range(num_islands)} # Only "improved" events per island |
| 56 | +island_not_replaced = {i: defaultdict(int) for i in range(num_islands)} # Only "not replaced" events per island |
| 57 | + |
| 58 | +# Track cumulative events over iterations per island |
| 59 | +island_cumulative_new = {i: [] for i in range(num_islands)} # (iteration, count) |
| 60 | +island_cumulative_improved = {i: [] for i in range(num_islands)} # (iteration, count) |
| 61 | +island_cumulative_not_replaced = {i: [] for i in range(num_islands)} # (iteration, count) |
| 62 | + |
| 63 | +# Current iteration and counts per island |
| 64 | +current_iteration = 0 |
| 65 | +island_new_count = {i: 0 for i in range(num_islands)} |
| 66 | +island_improved_count = {i: 0 for i in range(num_islands)} |
| 67 | +island_not_replaced_count = {i: 0 for i in range(num_islands)} |
| 68 | + |
| 69 | +pattern = re.compile(r"\{'complexity': (\d+), 'diversity': (\d+)\}") |
| 70 | +iteration_pattern = re.compile(r"Iteration (\d+):") |
| 71 | +island_pattern = re.compile(r"island (\d+)") # Matches "Island 0", "Island 1", etc. |
| 72 | + |
| 73 | +with open(log_path) as f: |
| 74 | + for line in f: |
| 75 | + # Check for iteration number - this marks the end of an iteration |
| 76 | + iter_match = iteration_pattern.search(line) |
| 77 | + if iter_match: |
| 78 | + # Save cumulative counts for the iteration that just completed (all islands) |
| 79 | + iteration_num = int(iter_match.group(1)) |
| 80 | + if iteration_num > 0: |
| 81 | + for island_id in range(num_islands): |
| 82 | + if island_new_count[island_id] > 0 or island_improved_count[island_id] > 0 or island_not_replaced_count[island_id] > 0: |
| 83 | + # Check if we already added this iteration for this island |
| 84 | + existing = island_cumulative_new[island_id] |
| 85 | + if not existing or existing[-1][0] != iteration_num: |
| 86 | + island_cumulative_new[island_id].append((iteration_num, island_new_count[island_id])) |
| 87 | + island_cumulative_improved[island_id].append((iteration_num, island_improved_count[island_id])) |
| 88 | + island_cumulative_not_replaced[island_id].append((iteration_num, island_not_replaced_count[island_id])) |
| 89 | + current_iteration = iteration_num |
| 90 | + |
| 91 | + # Check for MAP-Elites events (these happen during the iteration) |
| 92 | + if m := pattern.search(line): |
| 93 | + complexity = int(m.group(1)) |
| 94 | + diversity = int(m.group(2)) |
| 95 | + coord = (complexity, diversity) |
| 96 | + |
| 97 | + # Extract island number from the line |
| 98 | + island_match = island_pattern.search(line) |
| 99 | + if island_match: |
| 100 | + island_id = int(island_match.group(1)) |
| 101 | + print(f"Island {island_id} found in line: {line}") |
| 102 | + if island_id >= num_islands: |
| 103 | + # Skip if island ID is out of range |
| 104 | + continue |
| 105 | + else: |
| 106 | + # If no island found, default to island 0 (backward compatibility) |
| 107 | + island_id = 0 |
| 108 | + |
| 109 | + # Count all events for this island |
| 110 | + island_usage[island_id][coord] += 1 |
| 111 | + |
| 112 | + # Check for specific event types and update cumulative counts |
| 113 | + if "New MAP-Elites cell occupied" in line: |
| 114 | + island_new_count[island_id] += 1 |
| 115 | + elif "improved" in line: |
| 116 | + island_improved[island_id][coord] += 1 |
| 117 | + island_improved_count[island_id] += 1 |
| 118 | + elif "not replaced" in line: |
| 119 | + island_not_replaced[island_id][coord] += 1 |
| 120 | + island_not_replaced_count[island_id] += 1 |
| 121 | + |
| 122 | +# Add final iteration counts if we have events but no final iteration marker |
| 123 | +if current_iteration > 0: |
| 124 | + for island_id in range(num_islands): |
| 125 | + if island_new_count[island_id] > 0 or island_improved_count[island_id] > 0 or island_not_replaced_count[island_id] > 0: |
| 126 | + existing = island_cumulative_new[island_id] |
| 127 | + if not existing or existing[-1][0] != current_iteration: |
| 128 | + island_cumulative_new[island_id].append((current_iteration, island_new_count[island_id])) |
| 129 | + island_cumulative_improved[island_id].append((current_iteration, island_improved_count[island_id])) |
| 130 | + island_cumulative_not_replaced[island_id].append((current_iteration, island_not_replaced_count[island_id])) |
| 131 | + |
| 132 | +def create_grid(data_dict, bins): |
| 133 | + """Create a 2D grid from coordinate dictionary""" |
| 134 | + grid = np.zeros((bins, bins), dtype=int) |
| 135 | + for (c, d), count in data_dict.items(): |
| 136 | + if 0 <= c < bins and 0 <= d < bins: |
| 137 | + grid[d, c] = count |
| 138 | + return grid |
| 139 | + |
| 140 | +# Font sizes |
| 141 | +label_fontsize = 14 |
| 142 | +title_fontsize = 16 |
| 143 | +tick_fontsize = 12 |
| 144 | +annot_fontsize = 10 |
| 145 | +cbar_label_fontsize = 12 |
| 146 | +legend_fontsize = 12 |
| 147 | + |
| 148 | +# Create plots for each island |
| 149 | +for island_id in range(num_islands): |
| 150 | + # Create grids for each event type for this island |
| 151 | + grid_all = create_grid(island_usage[island_id], bins) |
| 152 | + grid_improved = create_grid(island_improved[island_id], bins) |
| 153 | + grid_not_replaced = create_grid(island_not_replaced[island_id], bins) |
| 154 | + |
| 155 | + # Create four plots (2x2 layout) for this island |
| 156 | + fig, axes = plt.subplots(2, 2, figsize=(20, 16)) |
| 157 | + axes = axes.flatten() # Flatten to 1D array for easier indexing |
| 158 | + |
| 159 | + # Plot 1: All events |
| 160 | + sns.heatmap(grid_all, annot=True, fmt='d', cmap='YlOrRd', cbar_kws={'label': 'Count'}, ax=axes[0], annot_kws={'size': annot_fontsize}) |
| 161 | + axes[0].set_xlabel('Complexity Bin', fontsize=label_fontsize) |
| 162 | + axes[0].set_ylabel('Diversity Bin', fontsize=label_fontsize) |
| 163 | + axes[0].set_title(f'Island {island_id}: All MAP-Elites Events\nTotal: {int(grid_all.sum())}', fontsize=title_fontsize) |
| 164 | + axes[0].tick_params(axis='both', labelsize=tick_fontsize) |
| 165 | + axes[0].invert_yaxis() |
| 166 | + |
| 167 | + # Plot 2: Improved events |
| 168 | + sns.heatmap(grid_improved, annot=True, fmt='d', cmap='Greens', cbar_kws={'label': 'Count'}, ax=axes[1], annot_kws={'size': annot_fontsize}) |
| 169 | + axes[1].set_xlabel('Complexity Bin', fontsize=label_fontsize) |
| 170 | + axes[1].set_ylabel('Diversity Bin', fontsize=label_fontsize) |
| 171 | + axes[1].set_title(f'Island {island_id}: Cell Improved Events\nTotal: {int(grid_improved.sum())}', fontsize=title_fontsize) |
| 172 | + axes[1].tick_params(axis='both', labelsize=tick_fontsize) |
| 173 | + axes[1].invert_yaxis() |
| 174 | + |
| 175 | + # Plot 3: Not replaced events |
| 176 | + sns.heatmap(grid_not_replaced, annot=True, fmt='d', cmap='Reds', cbar_kws={'label': 'Count'}, ax=axes[2], annot_kws={'size': annot_fontsize}) |
| 177 | + axes[2].set_xlabel('Complexity Bin', fontsize=label_fontsize) |
| 178 | + axes[2].set_ylabel('Diversity Bin', fontsize=label_fontsize) |
| 179 | + axes[2].set_title(f'Island {island_id}: Cell Not Replaced Events\nTotal: {int(grid_not_replaced.sum())}', fontsize=title_fontsize) |
| 180 | + axes[2].tick_params(axis='both', labelsize=tick_fontsize) |
| 181 | + axes[2].invert_yaxis() |
| 182 | + |
| 183 | + # Plot 4: Cumulative events over iterations |
| 184 | + cumulative_new = island_cumulative_new[island_id] |
| 185 | + cumulative_improved = island_cumulative_improved[island_id] |
| 186 | + cumulative_not_replaced = island_cumulative_not_replaced[island_id] |
| 187 | + |
| 188 | + if cumulative_new or cumulative_improved or cumulative_not_replaced: |
| 189 | + # Get all unique iterations |
| 190 | + all_iterations = set() |
| 191 | + if cumulative_new: |
| 192 | + all_iterations.update(x[0] for x in cumulative_new) |
| 193 | + if cumulative_improved: |
| 194 | + all_iterations.update(x[0] for x in cumulative_improved) |
| 195 | + if cumulative_not_replaced: |
| 196 | + all_iterations.update(x[0] for x in cumulative_not_replaced) |
| 197 | + |
| 198 | + iterations = sorted(all_iterations) |
| 199 | + |
| 200 | + # Create dictionaries for quick lookup |
| 201 | + new_dict = dict(cumulative_new) |
| 202 | + improved_dict = dict(cumulative_improved) |
| 203 | + not_replaced_dict = dict(cumulative_not_replaced) |
| 204 | + |
| 205 | + # Build aligned arrays |
| 206 | + new_counts = [new_dict.get(iter, 0) for iter in iterations] |
| 207 | + improved_counts = [improved_dict.get(iter, 0) for iter in iterations] |
| 208 | + not_replaced_counts = [not_replaced_dict.get(iter, 0) for iter in iterations] |
| 209 | + |
| 210 | + axes[3].plot(iterations, new_counts, 'o-', linewidth=2, markersize=4, label='New Cell Occupied', color='orange') |
| 211 | + axes[3].plot(iterations, improved_counts, 's-', linewidth=2, markersize=4, label='Cell Improved', color='green') |
| 212 | + axes[3].plot(iterations, not_replaced_counts, '^-', linewidth=2, markersize=4, label='Cell Not Replaced', color='red') |
| 213 | + axes[3].set_xlabel('Iteration', fontsize=label_fontsize) |
| 214 | + axes[3].set_ylabel('Cumulative Count', fontsize=label_fontsize) |
| 215 | + axes[3].set_title(f'Island {island_id}: Cumulative MAP-Elites Events Over Iterations', fontsize=title_fontsize) |
| 216 | + axes[3].tick_params(axis='both', labelsize=tick_fontsize) |
| 217 | + axes[3].legend(fontsize=legend_fontsize) |
| 218 | + axes[3].grid(True, alpha=0.3) |
| 219 | + else: |
| 220 | + axes[3].text(0.5, 0.5, 'No iteration data found', ha='center', va='center', fontsize=label_fontsize) |
| 221 | + axes[3].set_title(f'Island {island_id}: Cumulative MAP-Elites Events Over Iterations', fontsize=title_fontsize) |
| 222 | + |
| 223 | + # Set colorbar label and tick font sizes for all colorbars (only for heatmaps) |
| 224 | + for ax in fig.axes: |
| 225 | + if hasattr(ax, 'yaxis') and ax.yaxis.label.get_text() == 'Count': |
| 226 | + ax.yaxis.label.set_fontsize(cbar_label_fontsize) |
| 227 | + ax.tick_params(labelsize=tick_fontsize) |
| 228 | + |
| 229 | + plt.tight_layout() |
| 230 | + |
| 231 | + # Save with island-specific filename |
| 232 | + if num_islands > 1: |
| 233 | + output_path = folder / "openevolve_output" / "logs" / f"cell_usage_island_{island_id}.png" |
| 234 | + else: |
| 235 | + # For single island, use the original filename for backward compatibility |
| 236 | + output_path = folder / "openevolve_output" / "logs" / "cell_usage.png" |
| 237 | + |
| 238 | + plt.savefig(output_path, dpi=150) |
| 239 | + print(f"Saved Island {island_id} plot to {output_path}") |
| 240 | + plt.close() # Close figure to free memory |
| 241 | + |
0 commit comments