feat: Add Analysis page, run comparison, notifications, and config editor
Dashboard enhancements:
- Add Analysis page with tabs: Overview, Parameters, Pareto, Correlations, Constraints, Surrogate, Runs
- Add PlotlyCorrelationHeatmap for parameter-objective correlation analysis
- Add PlotlyFeasibilityChart for constraint satisfaction visualization
- Add PlotlySurrogateQuality for FEA vs NN prediction comparison
- Add PlotlyRunComparison for comparing optimization runs within a study
Real-time improvements:
- Replace watchdog file-watching with SQLite database polling for better Windows reliability
- Add DatabasePoller class with 2-second polling interval
- Enhanced WebSocket messages: trial_completed, new_best, pareto_update, progress
Desktop notifications:
- Add useNotifications hook using Web Notifications API
- Add NotificationSettings toggle component
- Notify users when new best solutions are found
Config editor:
- Add PUT /studies/{study_id}/config endpoint with auto-backup
- Add ConfigEditor modal with tabs: General, Variables, Objectives, Settings, JSON
- Prevents editing while optimization is running
Enhanced Pareto visualization:
- Add dark mode styling with transparent backgrounds
- Add stats bar showing Pareto, FEA, NN, and infeasible counts
- Add Pareto front connecting line for 2D view
- Add table showing top 10 Pareto-optimal solutions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
import { useMemo } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
||||
|
||||
interface Run {
|
||||
run_id: number;
|
||||
name: string;
|
||||
source: 'FEA' | 'NN';
|
||||
trial_count: number;
|
||||
best_value: number | null;
|
||||
avg_value: number | null;
|
||||
first_trial: string | null;
|
||||
last_trial: string | null;
|
||||
}
|
||||
|
||||
interface PlotlyRunComparisonProps {
|
||||
runs: Run[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function PlotlyRunComparison({ runs, height = 400 }: PlotlyRunComparisonProps) {
|
||||
const chartData = useMemo(() => {
|
||||
if (runs.length === 0) return null;
|
||||
|
||||
// Separate FEA and NN runs
|
||||
const feaRuns = runs.filter(r => r.source === 'FEA');
|
||||
const nnRuns = runs.filter(r => r.source === 'NN');
|
||||
|
||||
// Create bar chart for trial counts
|
||||
const trialCountData = {
|
||||
x: runs.map(r => r.name),
|
||||
y: runs.map(r => r.trial_count),
|
||||
type: 'bar' as const,
|
||||
name: 'Trial Count',
|
||||
marker: {
|
||||
color: runs.map(r => r.source === 'NN' ? 'rgba(147, 51, 234, 0.8)' : 'rgba(59, 130, 246, 0.8)'),
|
||||
line: { color: runs.map(r => r.source === 'NN' ? 'rgb(147, 51, 234)' : 'rgb(59, 130, 246)'), width: 1 }
|
||||
},
|
||||
hovertemplate: '<b>%{x}</b><br>Trials: %{y}<extra></extra>'
|
||||
};
|
||||
|
||||
// Create line chart for best values
|
||||
const bestValueData = {
|
||||
x: runs.map(r => r.name),
|
||||
y: runs.map(r => r.best_value),
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines+markers' as const,
|
||||
name: 'Best Value',
|
||||
yaxis: 'y2',
|
||||
line: { color: 'rgba(16, 185, 129, 1)', width: 2 },
|
||||
marker: { size: 8, color: 'rgba(16, 185, 129, 1)' },
|
||||
hovertemplate: '<b>%{x}</b><br>Best: %{y:.4e}<extra></extra>'
|
||||
};
|
||||
|
||||
return { trialCountData, bestValueData, feaRuns, nnRuns };
|
||||
}, [runs]);
|
||||
|
||||
// Calculate statistics
|
||||
const stats = useMemo(() => {
|
||||
if (runs.length === 0) return null;
|
||||
|
||||
const totalTrials = runs.reduce((sum, r) => sum + r.trial_count, 0);
|
||||
const feaTrials = runs.filter(r => r.source === 'FEA').reduce((sum, r) => sum + r.trial_count, 0);
|
||||
const nnTrials = runs.filter(r => r.source === 'NN').reduce((sum, r) => sum + r.trial_count, 0);
|
||||
|
||||
const bestValues = runs.map(r => r.best_value).filter((v): v is number => v !== null);
|
||||
const overallBest = bestValues.length > 0 ? Math.min(...bestValues) : null;
|
||||
|
||||
// Calculate improvement from first FEA run to overall best
|
||||
const feaRuns = runs.filter(r => r.source === 'FEA');
|
||||
const firstFEA = feaRuns.length > 0 ? feaRuns[0].best_value : null;
|
||||
const improvement = firstFEA && overallBest ? ((firstFEA - overallBest) / Math.abs(firstFEA)) * 100 : null;
|
||||
|
||||
return {
|
||||
totalTrials,
|
||||
feaTrials,
|
||||
nnTrials,
|
||||
overallBest,
|
||||
improvement,
|
||||
totalRuns: runs.length,
|
||||
feaRuns: runs.filter(r => r.source === 'FEA').length,
|
||||
nnRuns: runs.filter(r => r.source === 'NN').length
|
||||
};
|
||||
}, [runs]);
|
||||
|
||||
if (!chartData || !stats) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 text-dark-400">
|
||||
No run data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Stats Summary */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">Total Runs</div>
|
||||
<div className="text-xl font-bold text-white">{stats.totalRuns}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">Total Trials</div>
|
||||
<div className="text-xl font-bold text-white">{stats.totalTrials}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">FEA Trials</div>
|
||||
<div className="text-xl font-bold text-blue-400">{stats.feaTrials}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">NN Trials</div>
|
||||
<div className="text-xl font-bold text-purple-400">{stats.nnTrials}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">Best Value</div>
|
||||
<div className="text-xl font-bold text-green-400">
|
||||
{stats.overallBest !== null ? stats.overallBest.toExponential(3) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">Improvement</div>
|
||||
<div className="text-xl font-bold text-primary-400 flex items-center gap-1">
|
||||
{stats.improvement !== null ? (
|
||||
<>
|
||||
{stats.improvement > 0 ? <TrendingDown className="w-4 h-4" /> :
|
||||
stats.improvement < 0 ? <TrendingUp className="w-4 h-4" /> :
|
||||
<Minus className="w-4 h-4" />}
|
||||
{Math.abs(stats.improvement).toFixed(1)}%
|
||||
</>
|
||||
) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<Plot
|
||||
data={[chartData.trialCountData, chartData.bestValueData]}
|
||||
layout={{
|
||||
height,
|
||||
margin: { l: 60, r: 60, t: 40, b: 100 },
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
font: { color: '#9ca3af', size: 11 },
|
||||
showlegend: true,
|
||||
legend: {
|
||||
orientation: 'h',
|
||||
y: 1.12,
|
||||
x: 0.5,
|
||||
xanchor: 'center',
|
||||
bgcolor: 'transparent'
|
||||
},
|
||||
xaxis: {
|
||||
tickangle: -45,
|
||||
gridcolor: 'rgba(75, 85, 99, 0.3)',
|
||||
linecolor: 'rgba(75, 85, 99, 0.5)',
|
||||
tickfont: { size: 10 }
|
||||
},
|
||||
yaxis: {
|
||||
title: { text: 'Trial Count' },
|
||||
gridcolor: 'rgba(75, 85, 99, 0.3)',
|
||||
linecolor: 'rgba(75, 85, 99, 0.5)',
|
||||
zeroline: false
|
||||
},
|
||||
yaxis2: {
|
||||
title: { text: 'Best Value' },
|
||||
overlaying: 'y',
|
||||
side: 'right',
|
||||
gridcolor: 'rgba(75, 85, 99, 0.1)',
|
||||
linecolor: 'rgba(75, 85, 99, 0.5)',
|
||||
zeroline: false,
|
||||
tickformat: '.2e'
|
||||
},
|
||||
bargap: 0.3,
|
||||
hovermode: 'x unified'
|
||||
}}
|
||||
config={{
|
||||
displayModeBar: true,
|
||||
displaylogo: false,
|
||||
modeBarButtonsToRemove: ['select2d', 'lasso2d', 'autoScale2d']
|
||||
}}
|
||||
className="w-full"
|
||||
useResizeHandler
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
|
||||
{/* Runs Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-dark-600">
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Run Name</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
|
||||
<th className="text-right py-2 px-3 text-dark-400 font-medium">Trials</th>
|
||||
<th className="text-right py-2 px-3 text-dark-400 font-medium">Best Value</th>
|
||||
<th className="text-right py-2 px-3 text-dark-400 font-medium">Avg Value</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{runs.map((run) => {
|
||||
// Calculate duration if times available
|
||||
let duration = '-';
|
||||
if (run.first_trial && run.last_trial) {
|
||||
const start = new Date(run.first_trial);
|
||||
const end = new Date(run.last_trial);
|
||||
const diffMs = end.getTime() - start.getTime();
|
||||
const diffMins = Math.round(diffMs / 60000);
|
||||
if (diffMins < 60) {
|
||||
duration = `${diffMins}m`;
|
||||
} else {
|
||||
const hours = Math.floor(diffMins / 60);
|
||||
const mins = diffMins % 60;
|
||||
duration = `${hours}h ${mins}m`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={run.run_id} className="border-b border-dark-700 hover:bg-dark-750">
|
||||
<td className="py-2 px-3 font-mono text-white">{run.name}</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
run.source === 'NN'
|
||||
? 'bg-purple-500/20 text-purple-400'
|
||||
: 'bg-blue-500/20 text-blue-400'
|
||||
}`}>
|
||||
{run.source}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-white">{run.trial_count}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-green-400">
|
||||
{run.best_value !== null ? run.best_value.toExponential(4) : '-'}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-dark-300">
|
||||
{run.avg_value !== null ? run.avg_value.toExponential(4) : '-'}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-dark-400">{duration}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlotlyRunComparison;
|
||||
Reference in New Issue
Block a user