Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ docker-build:
docker-run:
docker run --rm -v $(PROJECT_DIR):/app --network="host" $(DOCKER_IMAGE) examples/function_minimization/initial_program.py examples/function_minimization/evaluator.py --config examples/function_minimization/config.yaml --iterations 1000

# Run the lm-eval benchmark
.PHONY: lm-eval
lm-eval:
$(PYTHON) scripts/lm_eval/lm-eval.py
# Run the visualization script
.PHONY: visualizer
visualizer:
$(PYTHON) scripts/visualizer.py --path examples/
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,21 @@ The script in `scripts/visualize.py` allows you to visualize the evolution tree
# Install requirements
pip install -r scripts/requirements.txt

# Start the visualization web server
# Start the visualization web server and have it watch the examples/ folder
python scripts/visualizer.py

# Start the visualization web server with a specific checkpoint
python scripts/visualizer.py --path examples/function_minimization/openevolve_output/checkpoints/checkpoint_100/
```

In the visualization UI, you can
- see the branching of your program evolution in a network visualization, with node radius chosen by the program fitness (= the currently selected metric),
- see the parent-child relationship of nodes and click through them in the sidebar (use the yellow locator icon in the sidebar to center the node in the graph),
- select the metric of interest (with the available metric choices depending on your data set),
- highlight nodes, for example the top score (for the chosen metric) or the MAP-elites members,
- click nodes to see their code and prompts (if available from the checkpoint data) in a sidebar,
- in the "Performance" tab, see their selected metric score vs generation in a graph

![OpenEvolve Visualizer](openevolve-visualizer.png)

### Docker
Expand Down
Binary file modified openevolve-visualizer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies = [
"pyyaml>=6.0",
"numpy>=1.22.0",
"tqdm>=4.64.0",
"flask",
]

[project.optional-dependencies]
Expand Down
19 changes: 19 additions & 0 deletions scripts/static/js/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function updateGraphNodeSelection() {
.attr('stroke', d => selectedProgramId === d.id ? 'red' : '#333')
.attr('stroke-width', d => selectedProgramId === d.id ? 3 : 1.5)
.classed('node-selected', d => selectedProgramId === d.id);
updateGraphEdgeSelection(); // update edge highlight when node selection changes
}

export function getNodeColor(d) {
Expand Down Expand Up @@ -104,6 +105,9 @@ export function selectProgram(programId) {
}
nodeElem.classed("node-hovered", false);
});
// Dispatch event for list view sync
window.dispatchEvent(new CustomEvent('node-selected', { detail: { id: programId } }));
updateGraphEdgeSelection(); // update edge highlight on selection
}

let svg = null;
Expand Down Expand Up @@ -252,6 +256,7 @@ function renderGraph(data, options = {}) {
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
updateGraphEdgeSelection(); // update edge highlight on tick
});

// Intelligent zoom/pan
Expand Down Expand Up @@ -309,6 +314,7 @@ function renderGraph(data, options = {}) {
}

selectProgram(selectedProgramId);
updateGraphEdgeSelection(); // update edge highlight after render
applyDragHandlersToAllNodes();

svg.on("click", function(event) {
Expand Down Expand Up @@ -385,6 +391,14 @@ export function centerAndHighlightNodeInGraph(nodeId) {
}
}

export function updateGraphEdgeSelection() {
if (!g) return;
g.selectAll('line')
.attr('stroke', d => (selectedProgramId && (d.source.id === selectedProgramId || d.target.id === selectedProgramId)) ? 'red' : '#999')
.attr('stroke-width', d => (selectedProgramId && (d.source.id === selectedProgramId || d.target.id === selectedProgramId)) ? 4 : 2)
.attr('stroke-opacity', d => (selectedProgramId && (d.source.id === selectedProgramId || d.target.id === selectedProgramId)) ? 0.95 : 0.6);
}

function dragstarted(event, d) {
if (!event.active && simulation) simulation.alphaTarget(0.3).restart(); // Keep simulation alive
d.fx = d.x;
Expand All @@ -400,4 +414,9 @@ function dragended(event, d) {
d.fy = null;
}

window.addEventListener('node-selected', function(e) {
// When node selection changes (e.g., from list view), update graph node selection
updateGraphNodeSelection();
});

export { renderGraph, g };
60 changes: 59 additions & 1 deletion scripts/static/js/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export function renderNodeList(nodes) {
<span class="summary-label">Average</span>
<span class="summary-value">${avgScore.toFixed(4)}</span>
${renderMetricBar(avgScore, minScore, maxScore)}
<span style="margin-left:1.2em;font-size:0.98em;color:#888;vertical-align:middle;">
<span title="Total programs, generations, islands">📦</span> Total: ${nodes.length} programs, ${new Set(nodes.map(n => n.generation)).size} generations, ${new Set(nodes.map(n => n.island)).size} islands
</span>
</div>
`;
container.innerHTML = '';
Expand Down Expand Up @@ -151,6 +154,12 @@ export function renderNodeList(nodes) {
}, 0);
container.appendChild(row);
});
container.focus();
// Scroll to selected node if present
const selected = container.querySelector('.node-list-item.selected');
if (selected) {
selected.scrollIntoView({behavior: 'smooth', block: 'center'});
}
}
export function selectListNodeById(id) {
setSelectedProgramId(id);
Expand Down Expand Up @@ -200,4 +209,53 @@ function showSidebarListView() {
} else {
showSidebar();
}
}
}

// Sync selection when switching to list tab
const tabListBtn = document.getElementById('tab-list');
if (tabListBtn) {
tabListBtn.addEventListener('click', () => {
renderNodeList(allNodeData);
});
}

// Keyboard navigation for up/down in list view
const nodeListContainer = document.getElementById('node-list-container');
if (nodeListContainer) {
nodeListContainer.tabIndex = 0;
nodeListContainer.addEventListener('keydown', function(e) {
if (!['ArrowUp', 'ArrowDown'].includes(e.key)) return;
e.preventDefault(); // Always prevent default to avoid browser scroll
const items = Array.from(nodeListContainer.querySelectorAll('.node-list-item'));
if (!items.length) return;
let idx = items.findIndex(item => item.classList.contains('selected'));
if (idx === -1) idx = 0;
if (e.key === 'ArrowUp' && idx > 0) idx--;
if (e.key === 'ArrowDown' && idx < items.length - 1) idx++;
const nextItem = items[idx];
if (nextItem) {
const nodeId = nextItem.getAttribute('data-node-id');
selectListNodeById(nodeId);
nextItem.focus();
nextItem.scrollIntoView({behavior: 'smooth', block: 'center'});
// Also scroll the page if needed
const rect = nextItem.getBoundingClientRect();
if (rect.top < 0 || rect.bottom > window.innerHeight) {
window.scrollTo({top: window.scrollY + rect.top - 100, behavior: 'smooth'});
}
}
});
// Focus container on click to enable keyboard nav
nodeListContainer.addEventListener('click', function() {
nodeListContainer.focus();
});
}

// Listen for node selection events from other views and sync selection in the list view
window.addEventListener('node-selected', function(e) {
// e.detail should contain the selected node id
if (e.detail && e.detail.id) {
setSelectedProgramId(e.detail.id);
renderNodeList(allNodeData);
}
});
52 changes: 47 additions & 5 deletions scripts/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { updateListSidebarLayout, renderNodeList } from './list.js';
import { renderGraph, g, getNodeRadius, animateGraphNodeAttributes } from './graph.js';

export let allNodeData = [];
let metricMinMax = {};

let archiveProgramIds = [];

Expand All @@ -13,15 +14,55 @@ const sidebarEl = document.getElementById('sidebar');
let lastDataStr = null;
let selectedProgramId = null;

function computeMetricMinMax(nodes) {
metricMinMax = {};
if (!nodes) return;
nodes.forEach(n => {
if (n.metrics && typeof n.metrics === 'object') {
for (const [k, v] of Object.entries(n.metrics)) {
if (typeof v === 'number' && isFinite(v)) {
if (!(k in metricMinMax)) {
metricMinMax[k] = {min: v, max: v};
} else {
metricMinMax[k].min = Math.min(metricMinMax[k].min, v);
metricMinMax[k].max = Math.max(metricMinMax[k].max, v);
}
}
}
}
});
// Avoid min==max
for (const k in metricMinMax) {
if (metricMinMax[k].min === metricMinMax[k].max) {
metricMinMax[k].min = 0;
metricMinMax[k].max = 1;
}
}
}

function formatMetrics(metrics) {
return Object.entries(metrics).map(([k, v]) => `<b>${k}</b>: ${v}`).join('<br>');
if (!metrics || typeof metrics !== 'object') return '';
let rows = Object.entries(metrics).map(([k, v]) => {
let min = 0, max = 1;
if (metricMinMax[k]) {
min = metricMinMax[k].min;
max = metricMinMax[k].max;
}
let valStr = (typeof v === 'number' && isFinite(v)) ? v.toFixed(4) : v;
return `<tr><td style='padding-right:0.7em;'><b>${k}</b></td><td style='padding-right:0.7em;'>${valStr}</td><td style='min-width:90px;'>${typeof v === 'number' ? renderMetricBar(v, min, max) : ''}</td></tr>`;
}).join('');
return `<table class='metrics-table'><tbody>${rows}</tbody></table>`;
}

function renderMetricBar(value, min, max, opts={}) {
let percent = 0;
if (typeof value === 'number' && isFinite(value) && max > min) {
percent = (value - min) / (max - min);
percent = Math.max(0, Math.min(1, percent));
if (typeof value === 'number' && isFinite(value)) {
if (max > min) {
percent = (value - min) / (max - min);
percent = Math.max(0, Math.min(1, percent));
} else if (max === min) {
percent = 1; // Show as filled if min==max
}
}
let minLabel = `<span class="metric-bar-min">${min.toFixed(2)}</span>`;
let maxLabel = `<span class="metric-bar-max">${max.toFixed(2)}</span>`;
Expand Down Expand Up @@ -201,10 +242,11 @@ document.getElementById('tab-branching').addEventListener('click', function() {
// Export all shared state and helpers for use in other modules
export function setAllNodeData(nodes) {
allNodeData = nodes;
computeMetricMinMax(nodes);
}

export function setSelectedProgramId(id) {
selectedProgramId = id;
}

export { archiveProgramIds, lastDataStr, selectedProgramId, formatMetrics, renderMetricBar, getHighlightNodes, getSelectedMetric };
export { archiveProgramIds, lastDataStr, selectedProgramId, formatMetrics, renderMetricBar, getHighlightNodes, getSelectedMetric, metricMinMax };
Loading