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:
757
atomizer-dashboard/frontend/src/pages/Analysis.tsx
Normal file
757
atomizer-dashboard/frontend/src/pages/Analysis.tsx
Normal file
@@ -0,0 +1,757 @@
|
||||
import { useState, useEffect, lazy, Suspense, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
BarChart3,
|
||||
TrendingUp,
|
||||
Grid3X3,
|
||||
Target,
|
||||
Filter,
|
||||
Brain,
|
||||
RefreshCw,
|
||||
Download,
|
||||
Layers,
|
||||
LucideIcon
|
||||
} from 'lucide-react';
|
||||
import { useStudy } from '../context/StudyContext';
|
||||
import { Card } from '../components/common/Card';
|
||||
|
||||
// Lazy load charts
|
||||
const PlotlyParetoPlot = lazy(() => import('../components/plotly/PlotlyParetoPlot').then(m => ({ default: m.PlotlyParetoPlot })));
|
||||
const PlotlyParallelCoordinates = lazy(() => import('../components/plotly/PlotlyParallelCoordinates').then(m => ({ default: m.PlotlyParallelCoordinates })));
|
||||
const PlotlyParameterImportance = lazy(() => import('../components/plotly/PlotlyParameterImportance').then(m => ({ default: m.PlotlyParameterImportance })));
|
||||
const PlotlyConvergencePlot = lazy(() => import('../components/plotly/PlotlyConvergencePlot').then(m => ({ default: m.PlotlyConvergencePlot })));
|
||||
const PlotlyCorrelationHeatmap = lazy(() => import('../components/plotly/PlotlyCorrelationHeatmap').then(m => ({ default: m.PlotlyCorrelationHeatmap })));
|
||||
const PlotlyFeasibilityChart = lazy(() => import('../components/plotly/PlotlyFeasibilityChart').then(m => ({ default: m.PlotlyFeasibilityChart })));
|
||||
const PlotlySurrogateQuality = lazy(() => import('../components/plotly/PlotlySurrogateQuality').then(m => ({ default: m.PlotlySurrogateQuality })));
|
||||
const PlotlyRunComparison = lazy(() => import('../components/plotly/PlotlyRunComparison').then(m => ({ default: m.PlotlyRunComparison })));
|
||||
|
||||
const ChartLoading = () => (
|
||||
<div className="flex items-center justify-center h-64 text-dark-400">
|
||||
<div className="animate-pulse">Loading chart...</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type AnalysisTab = 'overview' | 'parameters' | 'pareto' | 'correlations' | 'constraints' | 'surrogate' | 'runs';
|
||||
|
||||
interface RunData {
|
||||
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 TrialData {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
params: Record<string, number>;
|
||||
user_attrs?: Record<string, any>;
|
||||
constraint_satisfied?: boolean;
|
||||
source?: 'FEA' | 'NN' | 'V10_FEA';
|
||||
}
|
||||
|
||||
interface ObjectiveData {
|
||||
name: string;
|
||||
direction: 'minimize' | 'maximize';
|
||||
}
|
||||
|
||||
interface StudyMetadata {
|
||||
objectives?: ObjectiveData[];
|
||||
design_variables?: Array<{ name: string; min?: number; max?: number }>;
|
||||
sampler?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function Analysis() {
|
||||
const navigate = useNavigate();
|
||||
const { selectedStudy, isInitialized } = useStudy();
|
||||
const [activeTab, setActiveTab] = useState<AnalysisTab>('overview');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [trials, setTrials] = useState<TrialData[]>([]);
|
||||
const [metadata, setMetadata] = useState<StudyMetadata | null>(null);
|
||||
const [paretoFront, setParetoFront] = useState<any[]>([]);
|
||||
const [runs, setRuns] = useState<RunData[]>([]);
|
||||
|
||||
// Redirect if no study selected
|
||||
useEffect(() => {
|
||||
if (isInitialized && !selectedStudy) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [selectedStudy, navigate, isInitialized]);
|
||||
|
||||
// Load study data
|
||||
useEffect(() => {
|
||||
if (!selectedStudy) return;
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Load trial history
|
||||
const historyRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/history?limit=500`);
|
||||
const historyData = await historyRes.json();
|
||||
const trialsData = historyData.trials.map((t: any) => {
|
||||
let values: number[] = [];
|
||||
if (t.objectives && Array.isArray(t.objectives)) {
|
||||
values = t.objectives;
|
||||
} else if (t.objective !== null && t.objective !== undefined) {
|
||||
values = [t.objective];
|
||||
}
|
||||
const rawSource = t.source || t.user_attrs?.source || 'FEA';
|
||||
const source: 'FEA' | 'NN' | 'V10_FEA' = rawSource === 'NN' ? 'NN' : rawSource === 'V10_FEA' ? 'V10_FEA' : 'FEA';
|
||||
return {
|
||||
trial_number: t.trial_number,
|
||||
values,
|
||||
params: t.design_variables || {},
|
||||
user_attrs: t.user_attrs || {},
|
||||
constraint_satisfied: t.constraint_satisfied !== false,
|
||||
source
|
||||
};
|
||||
});
|
||||
setTrials(trialsData);
|
||||
|
||||
// Load metadata
|
||||
const metadataRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/metadata`);
|
||||
const metadataData = await metadataRes.json();
|
||||
setMetadata(metadataData);
|
||||
|
||||
// Load Pareto front
|
||||
const paretoRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/pareto-front`);
|
||||
const paretoData = await paretoRes.json();
|
||||
if (paretoData.is_multi_objective && paretoData.pareto_front) {
|
||||
setParetoFront(paretoData.pareto_front);
|
||||
}
|
||||
|
||||
// Load runs data for comparison
|
||||
const runsRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/runs`);
|
||||
const runsData = await runsRes.json();
|
||||
if (runsData.runs) {
|
||||
setRuns(runsData.runs);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load analysis data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [selectedStudy]);
|
||||
|
||||
// Calculate statistics
|
||||
const stats = useMemo(() => {
|
||||
if (trials.length === 0) return null;
|
||||
|
||||
const objectives = trials.map(t => t.values[0]).filter(v => v !== undefined && !isNaN(v));
|
||||
if (objectives.length === 0) return null;
|
||||
|
||||
const sorted = [...objectives].sort((a, b) => a - b);
|
||||
const min = sorted[0];
|
||||
const max = sorted[sorted.length - 1];
|
||||
const mean = objectives.reduce((a, b) => a + b, 0) / objectives.length;
|
||||
const median = sorted[Math.floor(sorted.length / 2)];
|
||||
const stdDev = Math.sqrt(objectives.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / objectives.length);
|
||||
const p25 = sorted[Math.floor(sorted.length * 0.25)];
|
||||
const p75 = sorted[Math.floor(sorted.length * 0.75)];
|
||||
const p90 = sorted[Math.floor(sorted.length * 0.90)];
|
||||
|
||||
const feaTrials = trials.filter(t => t.source === 'FEA').length;
|
||||
const nnTrials = trials.filter(t => t.source === 'NN').length;
|
||||
const feasible = trials.filter(t => t.constraint_satisfied).length;
|
||||
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
mean,
|
||||
median,
|
||||
stdDev,
|
||||
p25,
|
||||
p75,
|
||||
p90,
|
||||
feaTrials,
|
||||
nnTrials,
|
||||
feasible,
|
||||
total: trials.length,
|
||||
feasibilityRate: (feasible / trials.length) * 100
|
||||
};
|
||||
}, [trials]);
|
||||
|
||||
// Tabs configuration
|
||||
const tabs: { id: AnalysisTab; label: string; icon: LucideIcon; disabled?: boolean }[] = [
|
||||
{ id: 'overview', label: 'Overview', icon: BarChart3 },
|
||||
{ id: 'parameters', label: 'Parameters', icon: TrendingUp },
|
||||
{ id: 'pareto', label: 'Pareto', icon: Target, disabled: (metadata?.objectives?.length || 0) <= 1 },
|
||||
{ id: 'correlations', label: 'Correlations', icon: Grid3X3 },
|
||||
{ id: 'constraints', label: 'Constraints', icon: Filter },
|
||||
{ id: 'surrogate', label: 'Surrogate', icon: Brain, disabled: trials.filter(t => t.source === 'NN').length === 0 },
|
||||
{ id: 'runs', label: 'Runs', icon: Layers, disabled: runs.length <= 1 },
|
||||
];
|
||||
|
||||
// Export data
|
||||
const handleExportCSV = () => {
|
||||
if (trials.length === 0) return;
|
||||
|
||||
const paramNames = Object.keys(trials[0].params);
|
||||
const headers = ['trial', 'objective', ...paramNames, 'source', 'feasible'].join(',');
|
||||
const rows = trials.map(t => [
|
||||
t.trial_number,
|
||||
t.values[0],
|
||||
...paramNames.map(p => t.params[p]),
|
||||
t.source,
|
||||
t.constraint_satisfied
|
||||
].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 = `${selectedStudy?.id}_analysis.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
if (!isInitialized || !selectedStudy) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full mx-auto mb-4"></div>
|
||||
<p className="text-dark-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isMultiObjective = (metadata?.objectives?.length || 0) > 1;
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[2400px] mx-auto px-4">
|
||||
{/* Header */}
|
||||
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-primary-400">Analysis</h1>
|
||||
<p className="text-dark-400 text-sm">Deep analysis for {selectedStudy.name || selectedStudy.id}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-dark-700 hover:bg-dark-600 text-white rounded-lg transition-colors"
|
||||
disabled={trials.length === 0}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex gap-1 mb-6 border-b border-dark-600 overflow-x-auto">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => !tab.disabled && setActiveTab(tab.id)}
|
||||
disabled={tab.disabled}
|
||||
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
|
||||
activeTab === tab.id
|
||||
? 'text-primary-400 border-b-2 border-primary-400 -mb-[2px]'
|
||||
: tab.disabled
|
||||
? 'text-dark-600 cursor-not-allowed'
|
||||
: 'text-dark-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<RefreshCw className="w-8 h-8 animate-spin text-dark-400" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Overview Tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-6">
|
||||
{/* Summary Stats */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Total Trials</div>
|
||||
<div className="text-2xl font-bold text-white">{stats.total}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Best Value</div>
|
||||
<div className="text-2xl font-bold text-green-400">{stats.min.toExponential(3)}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Mean</div>
|
||||
<div className="text-2xl font-bold text-white">{stats.mean.toExponential(3)}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Median</div>
|
||||
<div className="text-2xl font-bold text-white">{stats.median.toExponential(3)}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Std Dev</div>
|
||||
<div className="text-2xl font-bold text-white">{stats.stdDev.toExponential(3)}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Feasibility</div>
|
||||
<div className="text-2xl font-bold text-primary-400">{stats.feasibilityRate.toFixed(1)}%</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Percentile Distribution */}
|
||||
{stats && (
|
||||
<Card title="Objective Distribution">
|
||||
<div className="grid grid-cols-4 gap-4 mb-4">
|
||||
<div className="text-center p-3 bg-dark-750 rounded-lg">
|
||||
<div className="text-xs text-dark-400 mb-1">Min</div>
|
||||
<div className="text-lg font-mono text-green-400">{stats.min.toExponential(3)}</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-dark-750 rounded-lg">
|
||||
<div className="text-xs text-dark-400 mb-1">25th %</div>
|
||||
<div className="text-lg font-mono text-white">{stats.p25.toExponential(3)}</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-dark-750 rounded-lg">
|
||||
<div className="text-xs text-dark-400 mb-1">75th %</div>
|
||||
<div className="text-lg font-mono text-white">{stats.p75.toExponential(3)}</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-dark-750 rounded-lg">
|
||||
<div className="text-xs text-dark-400 mb-1">90th %</div>
|
||||
<div className="text-lg font-mono text-white">{stats.p90.toExponential(3)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Convergence Plot */}
|
||||
{trials.length > 0 && (
|
||||
<Card title="Convergence Plot">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyConvergencePlot
|
||||
trials={trials}
|
||||
objectiveIndex={0}
|
||||
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
|
||||
direction="minimize"
|
||||
height={350}
|
||||
/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Best Trials Table */}
|
||||
<Card title="Top 10 Best Trials">
|
||||
<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">Rank</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Trial</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Objective</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
|
||||
{Object.keys(trials[0]?.params || {}).slice(0, 3).map(p => (
|
||||
<th key={p} className="text-left py-2 px-3 text-dark-400 font-medium">{p}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...trials]
|
||||
.sort((a, b) => (a.values[0] ?? Infinity) - (b.values[0] ?? Infinity))
|
||||
.slice(0, 10)
|
||||
.map((trial, idx) => (
|
||||
<tr key={trial.trial_number} className="border-b border-dark-700">
|
||||
<td className="py-2 px-3">
|
||||
<span className={`inline-flex w-6 h-6 items-center justify-center rounded-full text-xs font-bold ${
|
||||
idx === 0 ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
idx === 1 ? 'bg-gray-400/20 text-gray-300' :
|
||||
idx === 2 ? 'bg-orange-700/20 text-orange-400' :
|
||||
'bg-dark-600 text-dark-400'
|
||||
}`}>
|
||||
{idx + 1}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 font-mono text-white">#{trial.trial_number}</td>
|
||||
<td className="py-2 px-3 font-mono text-green-400">{trial.values[0]?.toExponential(4)}</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
trial.source === 'NN' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
|
||||
}`}>
|
||||
{trial.source}
|
||||
</span>
|
||||
</td>
|
||||
{Object.keys(trials[0]?.params || {}).slice(0, 3).map(p => (
|
||||
<td key={p} className="py-2 px-3 font-mono text-dark-300">
|
||||
{trial.params[p]?.toFixed(4)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Parameters Tab */}
|
||||
{activeTab === 'parameters' && (
|
||||
<div className="space-y-6">
|
||||
{/* Parameter Importance */}
|
||||
{trials.length > 0 && metadata?.design_variables && (
|
||||
<Card title="Parameter Importance">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyParameterImportance
|
||||
trials={trials}
|
||||
designVariables={metadata.design_variables}
|
||||
objectiveIndex={0}
|
||||
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
|
||||
height={400}
|
||||
/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Parallel Coordinates */}
|
||||
{trials.length > 0 && metadata && (
|
||||
<Card title="Parallel Coordinates">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyParallelCoordinates
|
||||
trials={trials}
|
||||
objectives={metadata.objectives || []}
|
||||
designVariables={metadata.design_variables || []}
|
||||
paretoFront={paretoFront}
|
||||
height={450}
|
||||
/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pareto Tab */}
|
||||
{activeTab === 'pareto' && isMultiObjective && (
|
||||
<div className="space-y-6">
|
||||
{/* Pareto Metrics */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Pareto Solutions</div>
|
||||
<div className="text-2xl font-bold text-primary-400">{paretoFront.length}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Objectives</div>
|
||||
<div className="text-2xl font-bold text-white">{metadata?.objectives?.length || 0}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Dominated Ratio</div>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{trials.length > 0 ? ((1 - paretoFront.length / trials.length) * 100).toFixed(1) : 0}%
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Pareto Front Plot */}
|
||||
{paretoFront.length > 0 && (
|
||||
<Card title="Pareto Front">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyParetoPlot
|
||||
trials={trials}
|
||||
paretoFront={paretoFront}
|
||||
objectives={metadata?.objectives || []}
|
||||
height={500}
|
||||
/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Pareto Solutions Table */}
|
||||
<Card title="Pareto-Optimal Solutions">
|
||||
<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">Trial</th>
|
||||
{metadata?.objectives?.map(obj => (
|
||||
<th key={obj.name} className="text-left py-2 px-3 text-dark-400 font-medium">{obj.name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paretoFront.slice(0, 20).map((sol, idx) => (
|
||||
<tr key={idx} className="border-b border-dark-700">
|
||||
<td className="py-2 px-3 font-mono text-white">#{sol.trial_number}</td>
|
||||
{sol.values?.map((v: number, i: number) => (
|
||||
<td key={i} className="py-2 px-3 font-mono text-primary-400">{v?.toExponential(4)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Correlations Tab */}
|
||||
{activeTab === 'correlations' && (
|
||||
<div className="space-y-6">
|
||||
{/* Correlation Heatmap */}
|
||||
{trials.length > 2 && (
|
||||
<Card title="Parameter-Objective Correlation Matrix">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyCorrelationHeatmap
|
||||
trials={trials}
|
||||
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
|
||||
height={Math.min(500, 100 + Object.keys(trials[0]?.params || {}).length * 40)}
|
||||
/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Correlation Interpretation Guide */}
|
||||
<Card title="Interpreting Correlations">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div className="p-3 bg-blue-500/10 rounded-lg border border-blue-500/30">
|
||||
<div className="text-blue-400 font-semibold mb-1">Strong Positive (0.7 to 1.0)</div>
|
||||
<p className="text-dark-400 text-xs">Increasing parameter increases objective</p>
|
||||
</div>
|
||||
<div className="p-3 bg-blue-500/5 rounded-lg border border-blue-500/20">
|
||||
<div className="text-blue-300 font-semibold mb-1">Moderate Positive (0.3 to 0.7)</div>
|
||||
<p className="text-dark-400 text-xs">Some positive relationship</p>
|
||||
</div>
|
||||
<div className="p-3 bg-red-500/5 rounded-lg border border-red-500/20">
|
||||
<div className="text-red-300 font-semibold mb-1">Moderate Negative (-0.7 to -0.3)</div>
|
||||
<p className="text-dark-400 text-xs">Some negative relationship</p>
|
||||
</div>
|
||||
<div className="p-3 bg-red-500/10 rounded-lg border border-red-500/30">
|
||||
<div className="text-red-400 font-semibold mb-1">Strong Negative (-1.0 to -0.7)</div>
|
||||
<p className="text-dark-400 text-xs">Increasing parameter decreases objective</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Top Correlations Table */}
|
||||
{trials.length > 2 && (
|
||||
<Card title="Strongest Parameter Correlations with Objective">
|
||||
<CorrelationTable trials={trials} objectiveName={metadata?.objectives?.[0]?.name || 'Objective'} />
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Constraints Tab */}
|
||||
{activeTab === 'constraints' && stats && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Feasible Trials</div>
|
||||
<div className="text-2xl font-bold text-green-400">{stats.feasible}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Infeasible Trials</div>
|
||||
<div className="text-2xl font-bold text-red-400">{stats.total - stats.feasible}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Feasibility Rate</div>
|
||||
<div className="text-2xl font-bold text-primary-400">{stats.feasibilityRate.toFixed(1)}%</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Feasibility Over Time Chart */}
|
||||
<Card title="Feasibility Rate Over Time">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyFeasibilityChart trials={trials} height={350} />
|
||||
</Suspense>
|
||||
</Card>
|
||||
|
||||
{/* Infeasible Trials List */}
|
||||
{stats.total - stats.feasible > 0 && (
|
||||
<Card title="Recent Infeasible Trials">
|
||||
<div className="overflow-x-auto max-h-64">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-dark-800">
|
||||
<tr className="border-b border-dark-600">
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Trial</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Objective</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trials
|
||||
.filter(t => !t.constraint_satisfied)
|
||||
.slice(-20)
|
||||
.reverse()
|
||||
.map(trial => (
|
||||
<tr key={trial.trial_number} className="border-b border-dark-700">
|
||||
<td className="py-2 px-3 font-mono text-white">#{trial.trial_number}</td>
|
||||
<td className="py-2 px-3 font-mono text-red-400">{trial.values[0]?.toExponential(4) || 'N/A'}</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
trial.source === 'NN' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
|
||||
}`}>
|
||||
{trial.source}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Surrogate Tab */}
|
||||
{activeTab === 'surrogate' && stats && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">FEA Evaluations</div>
|
||||
<div className="text-2xl font-bold text-blue-400">{stats.feaTrials}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">NN Predictions</div>
|
||||
<div className="text-2xl font-bold text-purple-400">{stats.nnTrials}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">NN Ratio</div>
|
||||
<div className="text-2xl font-bold text-green-400">
|
||||
{stats.nnTrials > 0 ? `${((stats.nnTrials / stats.total) * 100).toFixed(0)}%` : '0%'}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Speedup Factor</div>
|
||||
<div className="text-2xl font-bold text-primary-400">
|
||||
{stats.feaTrials > 0 ? `${(stats.total / stats.feaTrials).toFixed(1)}x` : '1.0x'}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Surrogate Quality Charts */}
|
||||
<Card title="Surrogate Model Analysis">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlySurrogateQuality trials={trials} height={400} />
|
||||
</Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Runs Tab */}
|
||||
{activeTab === 'runs' && runs.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
<Card title="Optimization Runs Comparison">
|
||||
<p className="text-dark-400 text-sm mb-4">
|
||||
Compare different optimization runs within this study. Studies with adaptive optimization
|
||||
may have multiple runs (e.g., initial FEA exploration, NN-accelerated iterations).
|
||||
</p>
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyRunComparison runs={runs} height={400} />
|
||||
</Suspense>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper component for correlation table
|
||||
function CorrelationTable({ trials, objectiveName }: { trials: TrialData[]; objectiveName: string }) {
|
||||
const correlations = useMemo(() => {
|
||||
if (trials.length < 3) return [];
|
||||
|
||||
const paramNames = Object.keys(trials[0].params);
|
||||
const objectives = trials.map(t => t.values[0]).filter(v => v !== undefined && !isNaN(v));
|
||||
|
||||
const results: { param: string; correlation: number; absCorr: number }[] = [];
|
||||
|
||||
paramNames.forEach(param => {
|
||||
const paramValues = trials.map(t => t.params[param]).filter(v => v !== undefined && !isNaN(v));
|
||||
const minLen = Math.min(paramValues.length, objectives.length);
|
||||
|
||||
if (minLen < 3) return;
|
||||
|
||||
// Calculate Pearson correlation
|
||||
const x = paramValues.slice(0, minLen);
|
||||
const y = objectives.slice(0, minLen);
|
||||
const n = x.length;
|
||||
|
||||
const meanX = x.reduce((a, b) => a + b, 0) / n;
|
||||
const meanY = y.reduce((a, b) => a + b, 0) / n;
|
||||
|
||||
let numerator = 0;
|
||||
let denomX = 0;
|
||||
let denomY = 0;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const dx = x[i] - meanX;
|
||||
const dy = y[i] - meanY;
|
||||
numerator += dx * dy;
|
||||
denomX += dx * dx;
|
||||
denomY += dy * dy;
|
||||
}
|
||||
|
||||
const denominator = Math.sqrt(denomX) * Math.sqrt(denomY);
|
||||
const corr = denominator === 0 ? 0 : numerator / denominator;
|
||||
|
||||
results.push({ param, correlation: corr, absCorr: Math.abs(corr) });
|
||||
});
|
||||
|
||||
return results.sort((a, b) => b.absCorr - a.absCorr);
|
||||
}, [trials]);
|
||||
|
||||
if (correlations.length === 0) {
|
||||
return <p className="text-dark-400 text-center py-4">Not enough data for correlation analysis</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<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">Parameter</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Correlation with {objectiveName}</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Strength</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{correlations.slice(0, 10).map(({ param, correlation, absCorr }) => (
|
||||
<tr key={param} className="border-b border-dark-700">
|
||||
<td className="py-2 px-3 font-mono text-white">{param}</td>
|
||||
<td className="py-2 px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-24 h-2 bg-dark-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${correlation > 0 ? 'bg-blue-500' : 'bg-red-500'}`}
|
||||
style={{ width: `${absCorr * 100}%`, marginLeft: correlation < 0 ? 'auto' : 0 }}
|
||||
/>
|
||||
</div>
|
||||
<span className={`font-mono ${
|
||||
absCorr > 0.7 ? 'text-white font-bold' :
|
||||
absCorr > 0.3 ? 'text-dark-200' : 'text-dark-400'
|
||||
}`}>
|
||||
{correlation > 0 ? '+' : ''}{correlation.toFixed(3)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
absCorr > 0.7 ? 'bg-primary-500/20 text-primary-400' :
|
||||
absCorr > 0.3 ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
'bg-dark-600 text-dark-400'
|
||||
}`}>
|
||||
{absCorr > 0.7 ? 'Strong' : absCorr > 0.3 ? 'Moderate' : 'Weak'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user