feat: Add Protocol 13 adaptive optimization, Plotly charts, and dashboard improvements

## Protocol 13: Adaptive Multi-Objective Optimization
- Iterative FEA + Neural Network surrogate workflow
- Initial FEA sampling, NN training, NN-accelerated search
- FEA validation of top NN predictions, retraining loop
- adaptive_state.json tracks iteration history and best values
- M1 mirror study (V11) with 103 FEA, 3000 NN trials

## Dashboard Visualization Enhancements
- Added Plotly.js interactive charts (parallel coords, Pareto, convergence)
- Lazy loading with React.lazy() for performance
- Code splitting: plotly.js-basic-dist (~1MB vs 3.5MB)
- Chart library toggle (Recharts default, Plotly on-demand)
- ExpandableChart component for full-screen modal views
- ConsoleOutput component for real-time log viewing

## Documentation
- Protocol 13 detailed documentation
- Dashboard visualization guide
- Plotly components README
- Updated run-optimization skill with Mode 5 (adaptive)

## Bug Fixes
- Fixed TypeScript errors in dashboard components
- Fixed Card component to accept ReactNode title
- Removed unused imports across components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Antoine
2025-12-04 07:41:54 -05:00
parent e74f1ccf36
commit 8cbdbcad78
270 changed files with 15471 additions and 517 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, lazy, Suspense } from 'react';
import {
LineChart, Line, ScatterChart, Scatter,
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell
@@ -8,14 +8,29 @@ import { apiClient } from '../api/client';
import { Card } from '../components/common/Card';
import { MetricCard } from '../components/dashboard/MetricCard';
import { StudyCard } from '../components/dashboard/StudyCard';
import { OptimizerPanel } from '../components/OptimizerPanel';
// import { OptimizerPanel } from '../components/OptimizerPanel'; // Not used currently
import { ParetoPlot } from '../components/ParetoPlot';
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
import { ParameterImportanceChart } from '../components/ParameterImportanceChart';
import { ConvergencePlot } from '../components/ConvergencePlot';
import { StudyReportViewer } from '../components/StudyReportViewer';
import { ConsoleOutput } from '../components/ConsoleOutput';
import { ExpandableChart } from '../components/ExpandableChart';
import type { Study, Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types';
// Lazy load Plotly components for better initial load performance
const PlotlyParallelCoordinates = lazy(() => import('../components/plotly/PlotlyParallelCoordinates').then(m => ({ default: m.PlotlyParallelCoordinates })));
const PlotlyParetoPlot = lazy(() => import('../components/plotly/PlotlyParetoPlot').then(m => ({ default: m.PlotlyParetoPlot })));
const PlotlyConvergencePlot = lazy(() => import('../components/plotly/PlotlyConvergencePlot').then(m => ({ default: m.PlotlyConvergencePlot })));
const PlotlyParameterImportance = lazy(() => import('../components/plotly/PlotlyParameterImportance').then(m => ({ default: m.PlotlyParameterImportance })));
// Loading placeholder for lazy components
const ChartLoading = () => (
<div className="flex items-center justify-center h-64 text-dark-400">
<div className="animate-pulse">Loading chart...</div>
</div>
);
export default function Dashboard() {
const [studies, setStudies] = useState<Study[]>([]);
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
@@ -37,6 +52,9 @@ export default function Dashboard() {
const [paretoFront, setParetoFront] = useState<any[]>([]);
const [allTrialsRaw, setAllTrialsRaw] = useState<any[]>([]); // All trials for parallel coordinates
// Chart library toggle: 'recharts' (faster) or 'plotly' (more interactive but slower)
const [chartLibrary, setChartLibrary] = useState<'plotly' | 'recharts'>('recharts');
// Load studies on mount
useEffect(() => {
apiClient.getStudies()
@@ -68,7 +86,7 @@ export default function Dashboard() {
};
// WebSocket connection
const { connectionStatus } = useOptimizationWebSocket({
const { connectionStatus: _connectionStatus } = useOptimizationWebSocket({
studyId: selectedStudyId,
onMessage: (msg) => {
if (msg.type === 'trial_completed') {
@@ -157,7 +175,8 @@ export default function Dashboard() {
values,
params: t.design_variables || {},
user_attrs: t.user_attrs || {},
constraint_satisfied: t.constraint_satisfied !== false
constraint_satisfied: t.constraint_satisfied !== false,
source: t.source || t.user_attrs?.source || 'FEA' // FEA vs NN differentiation
};
});
setAllTrialsRaw(trialsData);
@@ -288,7 +307,7 @@ export default function Dashboard() {
};
return (
<div className="container mx-auto">
<div className="w-full max-w-[2400px] mx-auto px-4">
{/* Alerts */}
<div className="fixed top-4 right-4 z-50 space-y-2">
{alerts.map(alert => (
@@ -332,6 +351,31 @@ export default function Dashboard() {
<button onClick={exportCSV} className="btn-secondary" disabled={allTrials.length === 0}>
Export CSV
</button>
{/* Chart library toggle */}
<div className="flex rounded-lg overflow-hidden border border-dark-500 ml-2">
<button
onClick={() => setChartLibrary('plotly')}
className={`px-3 py-1.5 text-sm transition-colors ${
chartLibrary === 'plotly'
? 'bg-primary-500 text-white'
: 'bg-dark-600 text-dark-200 hover:bg-dark-500'
}`}
title="Interactive Plotly charts with zoom, pan, and export"
>
Plotly
</button>
<button
onClick={() => setChartLibrary('recharts')}
className={`px-3 py-1.5 text-sm transition-colors ${
chartLibrary === 'recharts'
? 'bg-primary-500 text-white'
: 'bg-dark-600 text-dark-200 hover:bg-dark-500'
}`}
title="Simple Recharts visualization"
>
Simple
</button>
</div>
</div>
</header>
@@ -405,171 +449,255 @@ export default function Dashboard() {
</div>
</div>
</Card>
<ParetoPlot
paretoData={paretoFront}
objectives={studyMetadata.objectives}
allTrials={allTrialsRaw}
/>
<ExpandableChart
title="Pareto Front"
subtitle={`${paretoFront.length} Pareto-optimal solutions`}
>
{chartLibrary === 'plotly' ? (
<Suspense fallback={<ChartLoading />}>
<PlotlyParetoPlot
trials={allTrialsRaw}
paretoFront={paretoFront}
objectives={studyMetadata.objectives}
height={350}
/>
</Suspense>
) : (
<ParetoPlot
paretoData={paretoFront}
objectives={studyMetadata.objectives}
allTrials={allTrialsRaw}
/>
)}
</ExpandableChart>
</div>
)}
{/* Parallel Coordinates (full width for multi-objective) */}
{allTrialsRaw.length > 0 && studyMetadata && studyMetadata.objectives && studyMetadata.design_variables && (
<div className="mb-6">
<ParallelCoordinatesPlot
paretoData={allTrialsRaw}
objectives={studyMetadata.objectives}
designVariables={studyMetadata.design_variables}
paretoFront={paretoFront}
/>
<ExpandableChart
title="Parallel Coordinates Plot"
subtitle={`${allTrialsRaw.length} trials - Design Variables → Objectives`}
>
{chartLibrary === 'plotly' ? (
<Suspense fallback={<ChartLoading />}>
<PlotlyParallelCoordinates
trials={allTrialsRaw}
objectives={studyMetadata.objectives}
designVariables={studyMetadata.design_variables}
paretoFront={paretoFront}
height={450}
/>
</Suspense>
) : (
<ParallelCoordinatesPlot
paretoData={allTrialsRaw}
objectives={studyMetadata.objectives}
designVariables={studyMetadata.design_variables}
paretoFront={paretoFront}
/>
)}
</ExpandableChart>
</div>
)}
{/* New Enhanced Charts: Convergence + Parameter Importance */}
{/* Convergence Plot - Full Width */}
{allTrialsRaw.length > 0 && (
<div className="grid grid-cols-2 gap-6 mb-6">
<ConvergencePlot
trials={allTrialsRaw}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
direction="minimize"
/>
{/* Parameter Importance needs design_variables from metadata or inferred from trials */}
{(studyMetadata?.design_variables?.length > 0 || (allTrialsRaw[0]?.params && Object.keys(allTrialsRaw[0].params).length > 0)) && (
<ParameterImportanceChart
trials={allTrialsRaw}
designVariables={
studyMetadata?.design_variables?.length > 0
? studyMetadata.design_variables
: Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name }))
}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
/>
)}
<div className="mb-6">
<ExpandableChart
title="Convergence Plot"
subtitle={`Best ${studyMetadata?.objectives?.[0]?.name || 'Objective'} over ${allTrialsRaw.length} trials`}
>
{chartLibrary === 'plotly' ? (
<Suspense fallback={<ChartLoading />}>
<PlotlyConvergencePlot
trials={allTrialsRaw}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
direction="minimize"
height={350}
/>
</Suspense>
) : (
<ConvergencePlot
trials={allTrialsRaw}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
direction="minimize"
/>
)}
</ExpandableChart>
</div>
)}
{/* Parameter Importance - Full Width */}
{allTrialsRaw.length > 0 && (studyMetadata?.design_variables?.length > 0 || (allTrialsRaw[0]?.params && Object.keys(allTrialsRaw[0].params).length > 0)) && (
<div className="mb-6">
<ExpandableChart
title="Parameter Importance"
subtitle={`Correlation with ${studyMetadata?.objectives?.[0]?.name || 'Objective'}`}
>
{chartLibrary === 'plotly' ? (
<Suspense fallback={<ChartLoading />}>
<PlotlyParameterImportance
trials={allTrialsRaw}
designVariables={
studyMetadata?.design_variables?.length > 0
? studyMetadata.design_variables
: Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name }))
}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
height={350}
/>
</Suspense>
) : (
<ParameterImportanceChart
trials={allTrialsRaw}
designVariables={
studyMetadata?.design_variables?.length > 0
? studyMetadata.design_variables
: Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name }))
}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
/>
)}
</ExpandableChart>
</div>
)}
{/* Charts */}
<div className="grid grid-cols-2 gap-6 mb-6">
{/* Convergence Chart */}
<Card title="Convergence Plot">
{convergenceData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={convergenceData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis
dataKey="trial_number"
stroke="#94a3b8"
label={{ value: 'Trial Number', position: 'insideBottom', offset: -5, fill: '#94a3b8' }}
/>
<YAxis
stroke="#94a3b8"
label={{ value: 'Objective', angle: -90, position: 'insideLeft', fill: '#94a3b8' }}
/>
<Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: 'none', borderRadius: '8px' }}
labelStyle={{ color: '#e2e8f0' }}
/>
<Legend />
<Line
type="monotone"
dataKey="objective"
stroke="#60a5fa"
name="Objective"
dot={{ r: 3 }}
/>
<Line
type="monotone"
dataKey="best_so_far"
stroke="#10b981"
name="Best So Far"
strokeWidth={2}
dot={{ r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-64 flex items-center justify-center text-dark-300">
No trial data yet
</div>
)}
</Card>
{/* Parameter Space Chart with Selectable Axes */}
<Card title={
<div className="flex items-center justify-between w-full">
<span>Parameter Space</span>
{paramNames.length > 2 && (
<div className="flex items-center gap-2 text-sm">
<span className="text-dark-400">X:</span>
<select
value={paramXIndex}
onChange={(e) => setParamXIndex(Number(e.target.value))}
className="bg-dark-600 text-dark-100 px-2 py-1 rounded text-xs border border-dark-500"
>
{paramNames.map((name, idx) => (
<option key={idx} value={idx}>{name}</option>
))}
</select>
<span className="text-dark-400">Y:</span>
<select
value={paramYIndex}
onChange={(e) => setParamYIndex(Number(e.target.value))}
className="bg-dark-600 text-dark-100 px-2 py-1 rounded text-xs border border-dark-500"
>
{paramNames.map((name, idx) => (
<option key={idx} value={idx}>{name}</option>
))}
</select>
<ExpandableChart
title="Convergence Plot (Single Objective)"
subtitle={`${convergenceData.length} trials`}
>
<Card title="Convergence Plot">
{convergenceData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={convergenceData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis
dataKey="trial_number"
stroke="#94a3b8"
label={{ value: 'Trial Number', position: 'insideBottom', offset: -5, fill: '#94a3b8' }}
/>
<YAxis
stroke="#94a3b8"
label={{ value: 'Objective', angle: -90, position: 'insideLeft', fill: '#94a3b8' }}
/>
<Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: 'none', borderRadius: '8px' }}
labelStyle={{ color: '#e2e8f0' }}
/>
<Legend />
<Line
type="monotone"
dataKey="objective"
stroke="#60a5fa"
name="Objective"
dot={{ r: 3 }}
/>
<Line
type="monotone"
dataKey="best_so_far"
stroke="#10b981"
name="Best So Far"
strokeWidth={2}
dot={{ r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-64 flex items-center justify-center text-dark-300">
No trial data yet
</div>
)}
</div>
}>
{parameterSpaceData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis
type="number"
dataKey="x"
stroke="#94a3b8"
name={paramNames[paramXIndex] || 'X'}
label={{ value: paramNames[paramXIndex] || 'Parameter X', position: 'insideBottom', offset: -5, fill: '#94a3b8' }}
/>
<YAxis
type="number"
dataKey="y"
stroke="#94a3b8"
name={paramNames[paramYIndex] || 'Y'}
label={{ value: paramNames[paramYIndex] || 'Parameter Y', angle: -90, position: 'insideLeft', fill: '#94a3b8' }}
/>
<Tooltip
cursor={{ strokeDasharray: '3 3' }}
contentStyle={{ backgroundColor: '#1e293b', border: 'none', borderRadius: '8px' }}
labelStyle={{ color: '#e2e8f0' }}
formatter={(value: any, name: string) => {
if (name === 'objective') return [value.toFixed(4), 'Objective'];
return [value.toFixed(3), name];
}}
/>
<Scatter name="Trials" data={parameterSpaceData}>
{parameterSpaceData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.isBest ? '#10b981' : '#60a5fa'}
r={entry.isBest ? 8 : 5}
/>
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
) : (
<div className="h-64 flex items-center justify-center text-dark-300">
No trial data yet
</Card>
</ExpandableChart>
{/* Parameter Space Chart with Selectable Axes */}
<ExpandableChart
title="Parameter Space"
subtitle={`${parameterSpaceData.length} trials - ${paramNames[paramXIndex] || 'X'} vs ${paramNames[paramYIndex] || 'Y'}`}
>
<Card title={
<div className="flex items-center justify-between w-full">
<span>Parameter Space</span>
{paramNames.length > 2 && (
<div className="flex items-center gap-2 text-sm">
<span className="text-dark-400">X:</span>
<select
value={paramXIndex}
onChange={(e) => setParamXIndex(Number(e.target.value))}
className="bg-dark-600 text-dark-100 px-2 py-1 rounded text-xs border border-dark-500"
>
{paramNames.map((name, idx) => (
<option key={idx} value={idx}>{name}</option>
))}
</select>
<span className="text-dark-400">Y:</span>
<select
value={paramYIndex}
onChange={(e) => setParamYIndex(Number(e.target.value))}
className="bg-dark-600 text-dark-100 px-2 py-1 rounded text-xs border border-dark-500"
>
{paramNames.map((name, idx) => (
<option key={idx} value={idx}>{name}</option>
))}
</select>
</div>
)}
</div>
)}
</Card>
}>
{parameterSpaceData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis
type="number"
dataKey="x"
stroke="#94a3b8"
name={paramNames[paramXIndex] || 'X'}
label={{ value: paramNames[paramXIndex] || 'Parameter X', position: 'insideBottom', offset: -5, fill: '#94a3b8' }}
/>
<YAxis
type="number"
dataKey="y"
stroke="#94a3b8"
name={paramNames[paramYIndex] || 'Y'}
label={{ value: paramNames[paramYIndex] || 'Parameter Y', angle: -90, position: 'insideLeft', fill: '#94a3b8' }}
/>
<Tooltip
cursor={{ strokeDasharray: '3 3' }}
contentStyle={{ backgroundColor: '#1e293b', border: 'none', borderRadius: '8px' }}
labelStyle={{ color: '#e2e8f0' }}
formatter={(value: any, name: string) => {
if (name === 'objective') return [value.toFixed(4), 'Objective'];
return [value.toFixed(3), name];
}}
/>
<Scatter name="Trials" data={parameterSpaceData}>
{parameterSpaceData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.isBest ? '#10b981' : '#60a5fa'}
r={entry.isBest ? 8 : 5}
/>
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
) : (
<div className="h-64 flex items-center justify-center text-dark-300">
No trial data yet
</div>
)}
</Card>
</ExpandableChart>
</div>
{/* Trial History with Sort Controls */}
@@ -746,6 +874,15 @@ export default function Dashboard() {
)}
</div>
</Card>
{/* Console Output - at the bottom */}
<div className="mt-6">
<ConsoleOutput
studyId={selectedStudyId}
refreshInterval={2000}
maxLines={200}
/>
</div>
</main>
</div>
</div>