Skip to content

Commit 8683d05

Browse files
committed
Independent visualization script for the evolution tree from a checkpoint
1 parent 4b099e3 commit 8683d05

File tree

4 files changed

+293
-0
lines changed

4 files changed

+293
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,20 @@ diff -u checkpoints/checkpoint_10/best_program.py checkpoints/checkpoint_20/best
128128
# Compare metrics
129129
cat checkpoints/checkpoint_*/best_program_info.json | grep -A 10 metrics
130130
```
131+
132+
### Visualizing the evolution tree
133+
134+
The script in `scripts/visualize.py` allows you to visualize the evolution tree and display it in your webbrowser. The script watches live for the newest checkpoint directory in the examples/ folder structure and updates the graph. Alternatively, you can also provide a specific checkpoint folder with the `--path` parameter.
135+
136+
```bash
137+
# Install requirements
138+
pip install -r scripts/requirements.txt
139+
140+
# Start the visualization web server
141+
python scripts/visualizer.py
142+
```
143+
![OpenEvolve Visualizer](openevolve-visualizer.png)
144+
131145
### Docker
132146

133147
You can also install and execute via Docker:

openevolve-visualizer.png

103 KB
Loading

scripts/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
flask

scripts/visualizer.py

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import os
2+
import json
3+
import glob
4+
import logging
5+
from flask import Flask, render_template_string, jsonify
6+
from pathlib import Path
7+
8+
app = Flask(__name__)
9+
10+
# HTML template with D3.js for network visualization
11+
HTML_TEMPLATE = """
12+
<!DOCTYPE html>
13+
<html lang="en">
14+
<head>
15+
<meta charset="UTF-8">
16+
<title>OpenEvolve Evolution Visualizer</title>
17+
<script src="https://d3js.org/d3.v7.min.js"></script>
18+
<style>
19+
html, body { height: 100%; margin: 0; padding: 0; }
20+
body { font-family: Arial, sans-serif; background: #f7f7f7; height: 100vh; width: 100vw; }
21+
#graph { width: 100vw; height: 100vh; }
22+
.node circle { stroke: #fff; stroke-width: 2px; }
23+
.node text { pointer-events: none; font-size: 12px; }
24+
.link { stroke: #999; stroke-opacity: 0.6; }
25+
.tooltip {
26+
position: absolute;
27+
text-align: left;
28+
width: 400px;
29+
padding: 10px;
30+
font: 12px sans-serif;
31+
background: #fff;
32+
border: 1px solid #aaa;
33+
border-radius: 8px;
34+
pointer-events: none;
35+
box-shadow: 2px 2px 8px #aaa;
36+
z-index: 10;
37+
}
38+
pre { background: #f0f0f0; padding: 6px; border-radius: 4px; }
39+
</style>
40+
</head>
41+
<body>
42+
<h1>OpenEvolve Evolution Visualizer</h1>
43+
<div id="graph"></div>
44+
<script>
45+
let width = window.innerWidth;
46+
let height = window.innerHeight - document.querySelector('h1').offsetHeight;
47+
48+
const svg = d3.select("#graph").append("svg")
49+
.attr("width", width)
50+
.attr("height", height)
51+
.call(d3.zoom()
52+
.scaleExtent([0.1, 10])
53+
.on("zoom", (event) => {
54+
g.attr("transform", event.transform);
55+
}))
56+
.on("dblclick.zoom", null);
57+
58+
const g = svg.append("g");
59+
60+
const tooltip = d3.select("body").append("div")
61+
.attr("class", "tooltip")
62+
.style("opacity", 0);
63+
64+
let lastDataStr = null;
65+
let sticky = false;
66+
67+
function formatMetrics(metrics) {
68+
return Object.entries(metrics).map(([k, v]) => `<b>${k}</b>: ${v}`).join('<br>');
69+
}
70+
71+
function showTooltip(event, d) {
72+
tooltip.transition().duration(200).style("opacity", .95);
73+
tooltip.html(
74+
`<b>Program ID:</b> ${d.id}<br>` +
75+
`<b>Island:</b> ${d.island}<br>` +
76+
`<b>Generation:</b> ${d.generation}<br>` +
77+
`<b>Parent ID:</b> ${d.parent_id || 'None'}<br>` +
78+
`<b>Metrics:</b><br>${formatMetrics(d.metrics)}<br>` +
79+
`<b>Code:</b><pre>${d.code.replace(/</g, '&lt;')}</pre>`
80+
)
81+
.style("left", (event.pageX + 20) + "px")
82+
.style("top", (event.pageY - 20) + "px");
83+
}
84+
function showTooltipSticky(event, d) {
85+
sticky = true;
86+
showTooltip(event, d);
87+
}
88+
89+
function hideTooltip() {
90+
if (sticky) return;
91+
tooltip.transition().duration(300).style("opacity", 0);
92+
}
93+
function resetTooltip() {
94+
sticky = false;
95+
hideTooltip(true);
96+
}
97+
98+
function renderGraph(data) {
99+
g.selectAll("*").remove();
100+
const simulation = d3.forceSimulation(data.nodes)
101+
.force("link", d3.forceLink(data.edges).id(d => d.id).distance(80))
102+
.force("charge", d3.forceManyBody().strength(-200))
103+
.force("center", d3.forceCenter(width / 2, height / 2));
104+
105+
const link = g.append("g")
106+
.attr("stroke", "#999")
107+
.attr("stroke-opacity", 0.6)
108+
.selectAll("line")
109+
.data(data.edges)
110+
.enter().append("line")
111+
.attr("stroke-width", 2);
112+
113+
const node = g.append("g")
114+
.attr("stroke", "#fff")
115+
.attr("stroke-width", 1.5)
116+
.selectAll("circle")
117+
.data(data.nodes)
118+
.enter().append("circle")
119+
.attr("r", 16)
120+
.attr("fill", d => d.island !== undefined ? d3.schemeCategory10[d.island % 10] : "#888")
121+
.on("mouseover", showTooltip)
122+
.on("click", showTooltipSticky)
123+
.on("mouseout", hideTooltip)
124+
.call(d3.drag()
125+
.on("start", dragstarted)
126+
.on("drag", dragged)
127+
.on("end", dragended));
128+
129+
node.append("title").text(d => d.id);
130+
131+
simulation.on("tick", () => {
132+
link
133+
.attr("x1", d => d.source.x)
134+
.attr("y1", d => d.source.y)
135+
.attr("x2", d => d.target.x)
136+
.attr("y2", d => d.target.y);
137+
node
138+
.attr("cx", d => d.x)
139+
.attr("cy", d => d.y);
140+
});
141+
142+
function dragstarted(event, d) {
143+
if (!event.active) simulation.alphaTarget(0.3).restart();
144+
d.fx = d.x;
145+
d.fy = d.y;
146+
}
147+
function dragged(event, d) {
148+
d.fx = event.x;
149+
d.fy = event.y;
150+
}
151+
function dragended(event, d) {
152+
if (!event.active) simulation.alphaTarget(0);
153+
d.fx = null;
154+
d.fy = null;
155+
}
156+
}
157+
158+
// Add background click handler to reset tooltip
159+
svg.on("click", function(event) {
160+
// Only reset if the click target is the SVG itself (not a node)
161+
if (event.target === this) {
162+
resetTooltip();
163+
}
164+
});
165+
166+
function fetchAndRender() {
167+
fetch('/data')
168+
.then(resp => resp.json())
169+
.then(data => {
170+
const dataStr = JSON.stringify(data);
171+
if (dataStr !== lastDataStr) {
172+
renderGraph(data);
173+
lastDataStr = dataStr;
174+
}
175+
});
176+
}
177+
fetchAndRender();
178+
setInterval(fetchAndRender, 2000); // Live update every 2s
179+
180+
// Responsive resize
181+
function resize() {
182+
width = window.innerWidth;
183+
height = window.innerHeight - document.querySelector('h1').offsetHeight;
184+
svg.attr("width", width).attr("height", height);
185+
fetchAndRender();
186+
}
187+
window.addEventListener('resize', resize);
188+
</script>
189+
</body>
190+
</html>
191+
"""
192+
193+
logger = logging.getLogger("openevolve.visualizer")
194+
195+
def find_latest_checkpoint(base_folder):
196+
# Check whether the base folder is itself a checkpoint folder
197+
if os.path.basename(base_folder).startswith('checkpoint_'):
198+
return base_folder
199+
200+
checkpoint_folders = glob.glob('**/checkpoint_*', root_dir=base_folder, recursive=True)
201+
if not checkpoint_folders:
202+
logger.info(f"No checkpoint folders found in {base_folder}")
203+
return None
204+
checkpoint_folders = [os.path.join(base_folder, folder) for folder in checkpoint_folders]
205+
checkpoint_folders.sort(key=lambda x: os.path.getmtime(x), reverse=True)
206+
logger.debug(f"Found checkpoint folder: {checkpoint_folders[0]}")
207+
return checkpoint_folders[0]
208+
209+
def load_evolution_data(checkpoint_folder):
210+
meta_path = os.path.join(checkpoint_folder, 'metadata.json')
211+
programs_dir = os.path.join(checkpoint_folder, 'programs')
212+
if not os.path.exists(meta_path) or not os.path.exists(programs_dir):
213+
logger.info(f"Missing metadata.json or programs dir in {checkpoint_folder}")
214+
return {"nodes": [], "edges": []}
215+
with open(meta_path) as f:
216+
meta = json.load(f)
217+
218+
nodes = []
219+
id_to_program = {}
220+
for island_idx, id_list in enumerate(meta.get('islands', [])):
221+
for pid in id_list:
222+
prog_path = os.path.join(programs_dir, f"{pid}.json")
223+
if os.path.exists(prog_path):
224+
with open(prog_path) as pf:
225+
prog = json.load(pf)
226+
prog['island'] = island_idx
227+
nodes.append(prog)
228+
id_to_program[pid] = prog
229+
else:
230+
logger.debug(f"Program file not found: {prog_path}")
231+
232+
edges = []
233+
for prog in nodes:
234+
parent_id = prog.get('parent_id')
235+
if parent_id and parent_id in id_to_program:
236+
edges.append({"source": parent_id, "target": prog['id']})
237+
238+
logger.info(f"Loaded {len(nodes)} nodes and {len(edges)} edges from {checkpoint_folder}")
239+
return {"nodes": nodes, "edges": edges}
240+
241+
@app.route('/')
242+
def index():
243+
return render_template_string(HTML_TEMPLATE)
244+
245+
@app.route('/data')
246+
def data():
247+
base_folder = os.environ.get('EVOLVE_OUTPUT', 'examples/')
248+
checkpoint = find_latest_checkpoint(base_folder)
249+
if not checkpoint:
250+
logger.info(f"No checkpoints found in {base_folder}")
251+
return jsonify({"nodes": [], "edges": []})
252+
253+
logger.info(f"Loading data from checkpoint: {checkpoint}")
254+
data = load_evolution_data(checkpoint)
255+
logger.debug(f"Data: {data}")
256+
return jsonify(data)
257+
258+
if __name__ == '__main__':
259+
import argparse
260+
261+
parser = argparse.ArgumentParser(description='OpenEvolve Evolution Visualizer')
262+
parser.add_argument('--path', type=str, default='examples/',
263+
help='Path to openevolve_output or checkpoints folder')
264+
parser.add_argument('--host', type=str, default='127.0.0.1')
265+
parser.add_argument('--port', type=int, default=8080)
266+
parser.add_argument('--log-level', type=str, default='INFO',
267+
help='Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)')
268+
args = parser.parse_args()
269+
270+
log_level = getattr(logging, args.log_level.upper(), logging.INFO)
271+
logging.basicConfig(
272+
level=log_level,
273+
format='[%(asctime)s] %(levelname)s %(name)s: %(message)s'
274+
)
275+
276+
os.environ['EVOLVE_OUTPUT'] = args.path
277+
logger.info(f"Starting server at http://{args.host}:{args.port} with log level {args.log_level.upper()}")
278+
app.run(host=args.host, port=args.port, debug=True)

0 commit comments

Comments
 (0)