feat: Implement Protocol 13 - Real-Time Dashboard Tracking

Complete implementation of Protocol 13 featuring real-time web dashboard
for monitoring multi-objective optimization studies.

## New Features

### Backend (Python)
- Real-time tracking system with per-trial JSON writes
- New API endpoints for metadata, optimizer state, and Pareto fronts
- Unit inference from objective descriptions
- Multi-objective support using Optuna's best_trials API

### Frontend (React + TypeScript)
- OptimizerPanel: Real-time optimizer state (phase, strategy, progress)
- ParetoPlot: Pareto front visualization with normalization toggle
  - 3 modes: Raw, Min-Max [0-1], Z-Score standardization
  - Pareto front line connecting optimal points
- ParallelCoordinatesPlot: High-dimensional interactive visualization
  - Objectives + design variables on parallel axes
  - Click-to-select, hover-to-highlight
  - Color-coded feasibility
- Dynamic units throughout all visualizations

### Documentation
- Comprehensive Protocol 13 guide with architecture, data flow, usage

## Files Added
- `docs/PROTOCOL_13_DASHBOARD.md`
- `atomizer-dashboard/frontend/src/components/OptimizerPanel.tsx`
- `atomizer-dashboard/frontend/src/components/ParetoPlot.tsx`
- `atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx`
- `optimization_engine/realtime_tracking.py`

## Files Modified
- `atomizer-dashboard/frontend/src/pages/Dashboard.tsx`
- `atomizer-dashboard/backend/api/routes/optimization.py`
- `optimization_engine/intelligent_optimizer.py`

## Testing
- Tested with bracket_stiffness_optimization_V2 (30 trials, 20 Pareto solutions)
- Dashboard running on localhost:3001
- All P1 and P2 features verified working

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 15:58:00 -05:00
parent ca25fbdec5
commit f76bd52894
8 changed files with 2740 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
/**
* Intelligent Optimizer Panel - Protocol 13
* Displays real-time optimizer state: phase, strategy, progress, confidence
*/
import { useEffect, useState } from 'react';
interface OptimizerState {
available: boolean;
current_phase?: string;
current_strategy?: string;
trial_number?: number;
total_trials?: number;
is_multi_objective?: boolean;
latest_recommendation?: {
strategy: string;
confidence: number;
reasoning: string;
};
}
export function OptimizerPanel({ studyId }: { studyId: string }) {
const [state, setState] = useState<OptimizerState | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchState = async () => {
try {
const res = await fetch(`/api/optimization/studies/${studyId}/optimizer-state`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
setState(data);
setError(null);
} catch (err) {
console.error('Failed to fetch optimizer state:', err);
setError('Failed to load');
}
};
fetchState();
const interval = setInterval(fetchState, 1000); // Update every second
return () => clearInterval(interval);
}, [studyId]);
if (error) {
return (
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
<h3 className="text-lg font-semibold mb-4 text-dark-100">Intelligent Optimizer</h3>
<div className="text-dark-400 text-sm">
{error}
</div>
</div>
);
}
if (!state?.available) {
return null;
}
// Format phase name for display
const formatPhase = (phase?: string) => {
if (!phase) return 'Unknown';
return phase
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Format strategy name for display
const formatStrategy = (strategy?: string) => {
if (!strategy) return 'Not set';
return strategy.toUpperCase();
};
const progress = state.trial_number && state.total_trials
? (state.trial_number / state.total_trials) * 100
: 0;
return (
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
<h3 className="text-lg font-semibold mb-4 text-dark-100 flex items-center gap-2">
Intelligent Optimizer
{state.is_multi_objective && (
<span className="text-xs bg-purple-500/20 text-purple-300 px-2 py-1 rounded">
Multi-Objective
</span>
)}
</h3>
<div className="space-y-4">
{/* Phase */}
<div>
<div className="text-sm text-dark-300 mb-1">Phase</div>
<div className="text-lg font-semibold text-primary-400">
{formatPhase(state.current_phase)}
</div>
</div>
{/* Strategy */}
<div>
<div className="text-sm text-dark-300 mb-1">Current Strategy</div>
<div className="text-lg font-semibold text-blue-400">
{formatStrategy(state.current_strategy)}
</div>
</div>
{/* Progress */}
<div>
<div className="text-sm text-dark-300 mb-1">Progress</div>
<div className="text-lg text-dark-100">
{state.trial_number || 0} / {state.total_trials || 0} trials
</div>
<div className="w-full bg-dark-500 rounded-full h-2 mt-2">
<div
className="bg-primary-400 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
</div>
{/* Confidence (if available) */}
{state.latest_recommendation && (
<div>
<div className="text-sm text-dark-300 mb-1">Confidence</div>
<div className="flex items-center gap-2">
<div className="flex-1 bg-dark-500 rounded-full h-2">
<div
className="bg-green-400 h-2 rounded-full transition-all duration-300"
style={{
width: `${state.latest_recommendation.confidence * 100}%`
}}
/>
</div>
<span className="text-sm font-mono text-dark-200 min-w-[3rem] text-right">
{(state.latest_recommendation.confidence * 100).toFixed(0)}%
</span>
</div>
</div>
)}
{/* Reasoning (if available) */}
{state.latest_recommendation && (
<div>
<div className="text-sm text-dark-300 mb-1">Reasoning</div>
<div className="text-sm text-dark-100 bg-dark-800 rounded p-3 border border-dark-600">
{state.latest_recommendation.reasoning}
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,270 @@
/**
* Parallel Coordinates Plot - Protocol 13
* High-dimensional visualization for multi-objective Pareto fronts
* Shows objectives and design variables as parallel axes
*/
import { useState } from 'react';
interface ParetoTrial {
trial_number: number;
values: number[];
params: Record<string, number>;
constraint_satisfied?: boolean;
}
interface Objective {
name: string;
type: 'minimize' | 'maximize';
unit?: string;
}
interface DesignVariable {
name: string;
unit?: string;
min: number;
max: number;
}
interface ParallelCoordinatesPlotProps {
paretoData: ParetoTrial[];
objectives: Objective[];
designVariables: DesignVariable[];
}
export function ParallelCoordinatesPlot({
paretoData,
objectives,
designVariables
}: ParallelCoordinatesPlotProps) {
const [hoveredTrial, setHoveredTrial] = useState<number | null>(null);
const [selectedTrials, setSelectedTrials] = useState<Set<number>>(new Set());
if (paretoData.length === 0) {
return (
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
<h3 className="text-lg font-semibold mb-4 text-dark-100">Parallel Coordinates</h3>
<div className="h-96 flex items-center justify-center text-dark-300">
No Pareto front data yet
</div>
</div>
);
}
// Combine objectives and design variables into axes
const axes: Array<{name: string, label: string, type: 'objective' | 'param'}> = [
...objectives.map((obj, i) => ({
name: `obj_${i}`,
label: obj.unit ? `${obj.name} (${obj.unit})` : obj.name,
type: 'objective' as const
})),
...designVariables.map(dv => ({
name: dv.name,
label: dv.unit ? `${dv.name} (${dv.unit})` : dv.name,
type: 'param' as const
}))
];
// Normalize data to [0, 1] for each axis
const normalizedData = paretoData.map(trial => {
const allValues: number[] = [];
// Add objectives
trial.values.forEach(val => allValues.push(val));
// Add design variables
designVariables.forEach(dv => {
allValues.push(trial.params[dv.name]);
});
return {
trial_number: trial.trial_number,
values: allValues,
feasible: trial.constraint_satisfied !== false
};
});
// Calculate min/max for each axis
const ranges = axes.map((_, axisIdx) => {
const values = normalizedData.map(d => d.values[axisIdx]);
return {
min: Math.min(...values),
max: Math.max(...values)
};
});
// Normalize function
const normalize = (value: number, axisIdx: number): number => {
const range = ranges[axisIdx];
if (range.max === range.min) return 0.5;
return (value - range.min) / (range.max - range.min);
};
// Chart dimensions
const width = 800;
const height = 400;
const margin = { top: 80, right: 20, bottom: 40, left: 20 };
const plotWidth = width - margin.left - margin.right;
const plotHeight = height - margin.top - margin.bottom;
const axisSpacing = plotWidth / (axes.length - 1);
// Toggle trial selection
const toggleTrial = (trialNum: number) => {
const newSelected = new Set(selectedTrials);
if (newSelected.has(trialNum)) {
newSelected.delete(trialNum);
} else {
newSelected.add(trialNum);
}
setSelectedTrials(newSelected);
};
return (
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-dark-100">
Parallel Coordinates ({paretoData.length} solutions)
</h3>
{selectedTrials.size > 0 && (
<button
onClick={() => setSelectedTrials(new Set())}
className="text-xs px-3 py-1 bg-dark-600 hover:bg-dark-500 rounded text-dark-200"
>
Clear Selection ({selectedTrials.size})
</button>
)}
</div>
<svg width={width} height={height} className="overflow-visible">
<g transform={`translate(${margin.left}, ${margin.top})`}>
{/* Draw axes */}
{axes.map((axis, i) => {
const x = i * axisSpacing;
return (
<g key={axis.name} transform={`translate(${x}, 0)`}>
{/* Axis line */}
<line
y1={0}
y2={plotHeight}
stroke="#475569"
strokeWidth={2}
/>
{/* Axis label */}
<text
y={-10}
textAnchor="middle"
fill="#94a3b8"
fontSize={12}
className="select-none"
transform={`rotate(-45, 0, -10)`}
>
{axis.label}
</text>
{/* Min/max labels */}
<text
y={plotHeight + 15}
textAnchor="middle"
fill="#64748b"
fontSize={10}
>
{ranges[i].min.toFixed(2)}
</text>
<text
y={-25}
textAnchor="middle"
fill="#64748b"
fontSize={10}
>
{ranges[i].max.toFixed(2)}
</text>
</g>
);
})}
{/* Draw lines for each trial */}
{normalizedData.map(trial => {
const isHovered = hoveredTrial === trial.trial_number;
const isSelected = selectedTrials.has(trial.trial_number);
const isHighlighted = isHovered || isSelected;
// Build path
const pathData = axes.map((_, i) => {
const x = i * axisSpacing;
const normalizedY = normalize(trial.values[i], i);
const y = plotHeight * (1 - normalizedY);
return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
}).join(' ');
return (
<path
key={trial.trial_number}
d={pathData}
fill="none"
stroke={
isSelected ? '#fbbf24' :
trial.feasible ? '#10b981' : '#ef4444'
}
strokeWidth={isHighlighted ? 2.5 : 1}
opacity={
selectedTrials.size > 0
? (isSelected ? 1 : 0.1)
: (isHighlighted ? 1 : 0.4)
}
strokeLinecap="round"
strokeLinejoin="round"
className="transition-all duration-200 cursor-pointer"
onMouseEnter={() => setHoveredTrial(trial.trial_number)}
onMouseLeave={() => setHoveredTrial(null)}
onClick={() => toggleTrial(trial.trial_number)}
/>
);
})}
{/* Hover tooltip */}
{hoveredTrial !== null && (
<g transform={`translate(${plotWidth + 10}, 20)`}>
<rect
x={0}
y={0}
width={120}
height={60}
fill="#1e293b"
stroke="#334155"
strokeWidth={1}
rx={4}
/>
<text x={10} y={20} fill="#e2e8f0" fontSize={12} fontWeight="bold">
Trial #{hoveredTrial}
</text>
<text x={10} y={38} fill="#94a3b8" fontSize={10}>
Click to select
</text>
<text x={10} y={52} fill="#94a3b8" fontSize={10}>
{selectedTrials.has(hoveredTrial) ? '✓ Selected' : '○ Not selected'}
</text>
</g>
)}
</g>
</svg>
{/* Legend */}
<div className="flex gap-6 justify-center mt-4 text-sm">
<div className="flex items-center gap-2">
<div className="w-8 h-0.5 bg-green-400" />
<span className="text-dark-200">Feasible</span>
</div>
<div className="flex items-center gap-2">
<div className="w-8 h-0.5 bg-red-400" />
<span className="text-dark-200">Infeasible</span>
</div>
<div className="flex items-center gap-2">
<div className="w-8 h-0.5 bg-yellow-400" style={{ height: '2px' }} />
<span className="text-dark-200">Selected</span>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,247 @@
/**
* Pareto Front Plot - Protocol 13
* Visualizes Pareto-optimal solutions for multi-objective optimization
*/
import { useState } from 'react';
import { ScatterChart, Scatter, Line, XAxis, YAxis, CartesianGrid, Tooltip, Cell, ResponsiveContainer, Legend } from 'recharts';
interface ParetoTrial {
trial_number: number;
values: [number, number];
params: Record<string, number>;
constraint_satisfied?: boolean;
}
interface Objective {
name: string;
type: 'minimize' | 'maximize';
unit?: string;
}
interface ParetoPlotProps {
paretoData: ParetoTrial[];
objectives: Objective[];
}
type NormalizationMode = 'raw' | 'minmax' | 'zscore';
export function ParetoPlot({ paretoData, objectives }: ParetoPlotProps) {
const [normMode, setNormMode] = useState<NormalizationMode>('raw');
if (paretoData.length === 0) {
return (
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
<h3 className="text-lg font-semibold mb-4 text-dark-100">Pareto Front</h3>
<div className="h-64 flex items-center justify-center text-dark-300">
No Pareto front data yet
</div>
</div>
);
}
// Extract raw values
const rawData = paretoData.map(trial => ({
x: trial.values[0],
y: trial.values[1],
trial_number: trial.trial_number,
feasible: trial.constraint_satisfied !== false
}));
// Calculate statistics for normalization
const xValues = rawData.map(d => d.x);
const yValues = rawData.map(d => d.y);
const xMin = Math.min(...xValues);
const xMax = Math.max(...xValues);
const yMin = Math.min(...yValues);
const yMax = Math.max(...yValues);
const xMean = xValues.reduce((a, b) => a + b, 0) / xValues.length;
const yMean = yValues.reduce((a, b) => a + b, 0) / yValues.length;
const xStd = Math.sqrt(xValues.reduce((sum, val) => sum + Math.pow(val - xMean, 2), 0) / xValues.length);
const yStd = Math.sqrt(yValues.reduce((sum, val) => sum + Math.pow(val - yMean, 2), 0) / yValues.length);
// Normalize data based on selected mode
const normalizeX = (val: number): number => {
if (normMode === 'minmax') {
return xMax === xMin ? 0.5 : (val - xMin) / (xMax - xMin);
} else if (normMode === 'zscore') {
return xStd === 0 ? 0 : (val - xMean) / xStd;
}
return val; // raw
};
const normalizeY = (val: number): number => {
if (normMode === 'minmax') {
return yMax === yMin ? 0.5 : (val - yMin) / (yMax - yMin);
} else if (normMode === 'zscore') {
return yStd === 0 ? 0 : (val - yMean) / yStd;
}
return val; // raw
};
// Transform data with normalization
const data = rawData.map(d => ({
x: normalizeX(d.x),
y: normalizeY(d.y),
rawX: d.x,
rawY: d.y,
trial_number: d.trial_number,
feasible: d.feasible
}));
// Sort data by x-coordinate for Pareto front line
const sortedData = [...data].sort((a, b) => a.x - b.x);
// Get objective labels with normalization indicator
const normSuffix = normMode === 'minmax' ? ' [0-1]' : normMode === 'zscore' ? ' [z-score]' : '';
const xLabel = objectives[0]
? `${objectives[0].name}${objectives[0].unit ? ` (${objectives[0].unit})` : ''}${normSuffix}`
: `Objective 1${normSuffix}`;
const yLabel = objectives[1]
? `${objectives[1].name}${objectives[1].unit ? ` (${objectives[1].unit})` : ''}${normSuffix}`
: `Objective 2${normSuffix}`;
// Custom tooltip (always shows raw values)
const CustomTooltip = ({ active, payload }: any) => {
if (!active || !payload || payload.length === 0) return null;
const point = payload[0].payload;
return (
<div className="bg-dark-800 border border-dark-600 rounded p-3 shadow-lg">
<div className="text-sm font-semibold text-dark-100 mb-2">
Trial #{point.trial_number}
</div>
<div className="space-y-1 text-xs">
<div className="text-dark-200">
{objectives[0]?.name || 'Obj 1'}: <span className="font-mono">{point.rawX.toFixed(4)}</span>
</div>
<div className="text-dark-200">
{objectives[1]?.name || 'Obj 2'}: <span className="font-mono">{point.rawY.toFixed(4)}</span>
</div>
<div className={point.feasible ? 'text-green-400' : 'text-red-400'}>
{point.feasible ? '✓ Feasible' : '✗ Infeasible'}
</div>
</div>
</div>
);
};
return (
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-dark-100">
Pareto Front ({paretoData.length} solutions)
</h3>
{/* Normalization Toggle */}
<div className="flex gap-1 bg-dark-800 rounded p-1 border border-dark-600">
<button
onClick={() => setNormMode('raw')}
className={`px-3 py-1 text-xs rounded transition-colors ${
normMode === 'raw'
? 'bg-primary-500 text-white'
: 'text-dark-300 hover:text-dark-100'
}`}
>
Raw
</button>
<button
onClick={() => setNormMode('minmax')}
className={`px-3 py-1 text-xs rounded transition-colors ${
normMode === 'minmax'
? 'bg-primary-500 text-white'
: 'text-dark-300 hover:text-dark-100'
}`}
>
Min-Max
</button>
<button
onClick={() => setNormMode('zscore')}
className={`px-3 py-1 text-xs rounded transition-colors ${
normMode === 'zscore'
? 'bg-primary-500 text-white'
: 'text-dark-300 hover:text-dark-100'
}`}
>
Z-Score
</button>
</div>
</div>
<ResponsiveContainer width="100%" height={400}>
<ScatterChart margin={{ top: 20, right: 20, bottom: 60, left: 60 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis
type="number"
dataKey="x"
name={objectives[0]?.name || 'Objective 1'}
stroke="#94a3b8"
label={{
value: xLabel,
position: 'insideBottom',
offset: -45,
fill: '#94a3b8',
style: { fontSize: '14px' }
}}
tick={{ fill: '#94a3b8' }}
/>
<YAxis
type="number"
dataKey="y"
name={objectives[1]?.name || 'Objective 2'}
stroke="#94a3b8"
label={{
value: yLabel,
angle: -90,
position: 'insideLeft',
offset: -45,
fill: '#94a3b8',
style: { fontSize: '14px' }
}}
tick={{ fill: '#94a3b8' }}
/>
<Tooltip content={<CustomTooltip />} />
<Legend
verticalAlign="top"
height={36}
content={() => (
<div className="flex gap-4 justify-center mb-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-400" />
<span className="text-sm text-dark-200">Feasible</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-400" />
<span className="text-sm text-dark-200">Infeasible</span>
</div>
</div>
)}
/>
{/* Pareto front line */}
<Line
type="monotone"
data={sortedData}
dataKey="y"
stroke="#8b5cf6"
strokeWidth={2}
dot={false}
connectNulls={false}
isAnimationActive={false}
/>
<Scatter name="Pareto Front" data={data}>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.feasible ? '#10b981' : '#ef4444'}
r={entry.feasible ? 6 : 4}
opacity={entry.feasible ? 1 : 0.6}
/>
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
</div>
);
}

View File

@@ -0,0 +1,427 @@
import { useState, useEffect } from 'react';
import {
LineChart, Line, ScatterChart, Scatter,
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell
} from 'recharts';
import { useWebSocket } from '../hooks/useWebSocket';
import { Card } from '../components/Card';
import { MetricCard } from '../components/MetricCard';
import { StudyCard } from '../components/StudyCard';
import { OptimizerPanel } from '../components/OptimizerPanel';
import { ParetoPlot } from '../components/ParetoPlot';
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
import type { Study, Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types';
interface DashboardProps {
studies: Study[];
selectedStudyId: string | null;
onStudySelect: (studyId: string) => void;
}
export default function Dashboard({ studies, selectedStudyId, onStudySelect }: DashboardProps) {
const [trials, setTrials] = useState<Trial[]>([]);
const [allTrials, setAllTrials] = useState<Trial[]>([]);
const [bestValue, setBestValue] = useState<number>(Infinity);
const [prunedCount, setPrunedCount] = useState<number>(0);
const [alerts, setAlerts] = useState<Array<{ id: number; type: 'success' | 'warning'; message: string }>>([]);
const [alertIdCounter, setAlertIdCounter] = useState(0);
// Protocol 13: New state for metadata and Pareto front
const [studyMetadata, setStudyMetadata] = useState<any>(null);
const [paretoFront, setParetoFront] = useState<any[]>([]);
const showAlert = (type: 'success' | 'warning', message: string) => {
const id = alertIdCounter;
setAlertIdCounter(prev => prev + 1);
setAlerts(prev => [...prev, { id, type, message }]);
setTimeout(() => {
setAlerts(prev => prev.filter(a => a.id !== id));
}, 5000);
};
// WebSocket connection
const { isConnected } = useWebSocket({
studyId: selectedStudyId,
onTrialCompleted: (trial) => {
setTrials(prev => [trial, ...prev].slice(0, 20));
setAllTrials(prev => [...prev, trial]);
if (trial.objective < bestValue) {
setBestValue(trial.objective);
showAlert('success', `New best: ${trial.objective.toFixed(4)} (Trial #${trial.trial_number})`);
}
},
onNewBest: (trial) => {
console.log('New best trial:', trial);
},
onTrialPruned: (pruned) => {
setPrunedCount(prev => prev + 1);
showAlert('warning', `Trial #${pruned.trial_number} pruned: ${pruned.pruning_cause}`);
},
});
// Load initial trial history when study changes
useEffect(() => {
if (selectedStudyId) {
setTrials([]);
setAllTrials([]);
setBestValue(Infinity);
setPrunedCount(0);
// Fetch full history
fetch(`/api/optimization/studies/${selectedStudyId}/history`)
.then(res => res.json())
.then(data => {
const sortedTrials = data.trials.sort((a: Trial, b: Trial) => a.trial_number - b.trial_number);
setAllTrials(sortedTrials);
setTrials(sortedTrials.slice(-20).reverse());
if (sortedTrials.length > 0) {
const minObj = Math.min(...sortedTrials.map((t: Trial) => t.objective));
setBestValue(minObj);
}
})
.catch(err => console.error('Failed to load history:', err));
// Fetch pruning count
fetch(`/api/optimization/studies/${selectedStudyId}/pruning`)
.then(res => res.json())
.then(data => {
setPrunedCount(data.pruned_trials?.length || 0);
})
.catch(err => console.error('Failed to load pruning data:', err));
// Protocol 13: Fetch metadata
fetch(`/api/optimization/studies/${selectedStudyId}/metadata`)
.then(res => res.json())
.then(data => {
setStudyMetadata(data);
})
.catch(err => console.error('Failed to load metadata:', err));
// Protocol 13: Fetch Pareto front
fetch(`/api/optimization/studies/${selectedStudyId}/pareto-front`)
.then(res => res.json())
.then(data => {
if (data.is_multi_objective) {
setParetoFront(data.pareto_front);
} else {
setParetoFront([]);
}
})
.catch(err => console.error('Failed to load Pareto front:', err));
}
}, [selectedStudyId]);
// Prepare chart data
const convergenceData: ConvergenceDataPoint[] = allTrials.map((trial, idx) => ({
trial_number: trial.trial_number,
objective: trial.objective,
best_so_far: Math.min(...allTrials.slice(0, idx + 1).map(t => t.objective)),
}));
const parameterSpaceData: ParameterSpaceDataPoint[] = allTrials.map(trial => {
const params = Object.values(trial.design_variables);
return {
trial_number: trial.trial_number,
x: params[0] || 0,
y: params[1] || 0,
objective: trial.objective,
isBest: trial.objective === bestValue,
};
});
// Calculate average objective
const avgObjective = allTrials.length > 0
? allTrials.reduce((sum, t) => sum + t.objective, 0) / allTrials.length
: 0;
// Get parameter names
const paramNames = allTrials.length > 0 ? Object.keys(allTrials[0].design_variables) : [];
// Helper: Format parameter label with unit from metadata
const getParamLabel = (paramName: string, index: number): string => {
if (!studyMetadata?.design_variables) {
return paramName || `Parameter ${index + 1}`;
}
const dv = studyMetadata.design_variables.find((v: any) => v.name === paramName);
if (dv && dv.unit) {
return `${paramName} (${dv.unit})`;
}
return paramName || `Parameter ${index + 1}`;
};
// Export functions
const exportJSON = () => {
if (allTrials.length === 0) return;
const data = JSON.stringify(allTrials, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${selectedStudyId}_trials.json`;
a.click();
URL.revokeObjectURL(url);
showAlert('success', 'JSON exported successfully!');
};
const exportCSV = () => {
if (allTrials.length === 0) return;
const headers = ['trial_number', 'objective', ...paramNames].join(',');
const rows = allTrials.map(t => [
t.trial_number,
t.objective,
...paramNames.map(k => t.design_variables[k])
].join(','));
const csv = [headers, ...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${selectedStudyId}_trials.csv`;
a.click();
URL.revokeObjectURL(url);
showAlert('success', 'CSV exported successfully!');
};
return (
<div className="container mx-auto p-6">
{/* Alerts */}
<div className="fixed top-4 right-4 z-50 space-y-2">
{alerts.map(alert => (
<div
key={alert.id}
className={`px-4 py-3 rounded-lg shadow-lg transition-all duration-300 ${
alert.type === 'success'
? 'bg-green-900 border-l-4 border-green-400 text-green-100'
: 'bg-yellow-900 border-l-4 border-yellow-400 text-yellow-100'
}`}
>
{alert.message}
</div>
))}
</div>
{/* Header */}
<header className="mb-8 flex items-center justify-between border-b border-dark-500 pb-4">
<div>
<h1 className="text-3xl font-bold text-primary-400">Atomizer Dashboard</h1>
<p className="text-dark-200 mt-2">Real-time optimization monitoring</p>
</div>
<div className="flex gap-2">
<button onClick={exportJSON} className="btn-secondary" disabled={allTrials.length === 0}>
Export JSON
</button>
<button onClick={exportCSV} className="btn-secondary" disabled={allTrials.length === 0}>
Export CSV
</button>
</div>
</header>
<div className="grid grid-cols-12 gap-6">
{/* Sidebar - Study List */}
<aside className="col-span-3">
<Card title="Active Studies">
<div className="space-y-3 max-h-[calc(100vh-200px)] overflow-y-auto">
{studies.map(study => (
<StudyCard
key={study.id}
study={study}
isActive={study.id === selectedStudyId}
onClick={() => onStudySelect(study.id)}
/>
))}
</div>
</Card>
</aside>
{/* Main Content */}
<main className="col-span-9">
{/* Metrics Grid */}
<div className="grid grid-cols-4 gap-4 mb-6">
<MetricCard label="Total Trials" value={allTrials.length} />
<MetricCard
label="Best Value"
value={bestValue === Infinity ? '-' : bestValue.toFixed(4)}
valueColor="text-green-400"
/>
<MetricCard
label="Avg Objective"
value={avgObjective > 0 ? avgObjective.toFixed(4) : '-'}
valueColor="text-blue-400"
/>
<MetricCard
label="Connection"
value={isConnected ? 'Connected' : 'Disconnected'}
valueColor={isConnected ? 'text-green-400' : 'text-red-400'}
/>
</div>
<div className="grid grid-cols-4 gap-4 mb-6">
<MetricCard
label="Pruned"
value={prunedCount}
valueColor={prunedCount > 0 ? 'text-red-400' : 'text-green-400'}
/>
</div>
{/* Protocol 13: Intelligent Optimizer & Pareto Front */}
{selectedStudyId && (
<div className="grid grid-cols-2 gap-6 mb-6">
<OptimizerPanel studyId={selectedStudyId} />
{paretoFront.length > 0 && studyMetadata && (
<ParetoPlot
paretoData={paretoFront}
objectives={studyMetadata.objectives || []}
/>
)}
</div>
)}
{/* Parallel Coordinates (full width for multi-objective) */}
{paretoFront.length > 0 && studyMetadata && (
<div className="mb-6">
<ParallelCoordinatesPlot
paretoData={paretoFront}
objectives={studyMetadata.objectives || []}
designVariables={studyMetadata.design_variables || []}
/>
</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 */}
<Card title={`Parameter Space (${paramNames[0] || 'X'} vs ${paramNames[1] || 'Y'})`}>
{parameterSpaceData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis
type="number"
dataKey="x"
stroke="#94a3b8"
name={paramNames[0] || 'X'}
label={{ value: getParamLabel(paramNames[0], 0), position: 'insideBottom', offset: -5, fill: '#94a3b8' }}
/>
<YAxis
type="number"
dataKey="y"
stroke="#94a3b8"
name={paramNames[1] || 'Y'}
label={{ value: getParamLabel(paramNames[1], 1), 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>
</div>
{/* Trial Feed */}
<Card title="Recent Trials">
<div className="space-y-2 max-h-96 overflow-y-auto">
{trials.length > 0 ? (
trials.map(trial => (
<div
key={trial.trial_number}
className={`p-3 rounded-lg transition-all duration-200 ${
trial.objective === bestValue
? 'bg-green-900 border-l-4 border-green-400'
: 'bg-dark-500 hover:bg-dark-400'
}`}
>
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-primary-400">
Trial #{trial.trial_number}
</span>
<span className={`font-mono text-lg ${
trial.objective === bestValue ? 'text-green-400 font-bold' : 'text-dark-100'
}`}>
{trial.objective.toFixed(4)}
</span>
</div>
<div className="text-xs text-dark-200 flex flex-wrap gap-3">
{Object.entries(trial.design_variables).map(([key, val]) => (
<span key={key}>
<span className="text-dark-400">{key}:</span> {val.toFixed(3)}
</span>
))}
</div>
</div>
))
) : (
<div className="text-center py-8 text-dark-300">
No trials yet. Waiting for optimization to start...
</div>
)}
</div>
</Card>
</main>
</div>
</div>
);
}