Skip to content

Commit 923bcba

Browse files
author
Akshat Gupta
committed
Added logging and within-island stats and plots
1 parent b16a08c commit 923bcba

8 files changed

Lines changed: 1014 additions & 0 deletions

examples/circle_packing/config_adaptive_exploration_full.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ database:
4444
elite_selection_ratio: 0.3
4545
exploration_ratio: 0.2
4646
exploitation_ratio: 0.7
47+
feature_bins: 10
4748

4849
# Adaptive exploration settings
4950
use_adaptive_search: true

openevolve/database.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,35 @@ def add(
297297
# Program exists, compare fitness
298298
should_replace = self._is_better(program, self.programs[existing_program_id])
299299

300+
# Log when program is not replaced (same format as replacement logging)
301+
if not should_replace:
302+
coords_dict = {
303+
self.config.feature_dimensions[i]: feature_coords[i]
304+
for i in range(len(feature_coords))
305+
}
306+
existing_program = self.programs[existing_program_id]
307+
new_fitness = get_fitness_score(program.metrics, self.config.feature_dimensions)
308+
existing_fitness = get_fitness_score(
309+
existing_program.metrics, self.config.feature_dimensions
310+
)
311+
logger.info(
312+
"Island %d MAP-Elites cell not replaced: %s (fitness: %.3f <= %.3f)",
313+
island_idx,
314+
coords_dict,
315+
new_fitness,
316+
existing_fitness,
317+
)
318+
# Remove program from database since it doesn't replace anything
319+
# This prevents accumulation of programs that don't contribute to MAP-Elites
320+
# if program.id in self.programs:
321+
# del self.programs[program.id]
322+
# # Also remove from island set and archive if present
323+
# self.islands[island_idx].discard(program.id)
324+
# self.archive.discard(program.id)
325+
# logger.debug(f"Removed program {program.id} from database (did not replace MAP-Elites cell)")
326+
327+
#return program.id # Early return since program wasn't added
328+
300329
if should_replace:
301330
# Log significant MAP-Elites events
302331
coords_dict = {

plot_all.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
# Script to run all visualization scripts for a given example folder
3+
4+
# Set the circle packing folder here
5+
FOLDER="examples/circle_packing10"
6+
7+
# Or use command line argument if provided
8+
if [ $# -ge 1 ]; then
9+
FOLDER="$1"
10+
fi
11+
12+
echo "Running visualizations for: $FOLDER"
13+
echo ""
14+
15+
# Run cell usage visualization
16+
echo "1. Generating cell usage plot..."
17+
python3 visualize_cell_usage.py "$FOLDER"
18+
echo ""
19+
20+
# Run checkpoint analysis (different script for circle_packing vs others)
21+
echo "2. Generating checkpoint analysis plot..."
22+
if [[ "$FOLDER" == *"circle_packing"* ]]; then
23+
python3 visualize_checkpoints.py "$FOLDER"
24+
else
25+
python3 visualize_signal_processing.py "$FOLDER"
26+
fi
27+
echo ""
28+
29+
# Run checkpoint retention analysis
30+
echo "3. Generating checkpoint retention plot..."
31+
python3 visualize_checkpoint_retention.py "$FOLDER"
32+
echo ""
33+
34+
echo "All visualizations complete!"
35+
echo "Output saved to: $FOLDER/openevolve_output/logs/"
36+

visualize_cell_usage.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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

Comments
 (0)