Backend: - spec.py: New AtomizerSpec REST API endpoints - spec_manager.py: SpecManager service for unified config - interview_engine.py: Study creation interview logic - claude.py: Enhanced Claude API with context - optimization.py: Extended optimization endpoints - context_builder.py, session_manager.py: Improved services Frontend: - Chat components: Enhanced message rendering, tool call cards - Hooks: useClaudeCode, useSpecWebSocket, improved useChat - Pages: Updated Dashboard, Analysis, Insights, Setup, Home - Components: ParallelCoordinatesPlot, ParetoPlot improvements - App.tsx: Route updates for canvas/studio Infrastructure: - vite.config.ts: Build configuration updates - start/stop-dashboard.bat: Script improvements
840 lines
36 KiB
TypeScript
840 lines
36 KiB
TypeScript
import { useState, useEffect, 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';
|
|
import { ConvergencePlot } from '../components/ConvergencePlot';
|
|
import { ParameterImportanceChart } from '../components/ParameterImportanceChart';
|
|
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
|
|
import { ParetoPlot } from '../components/ParetoPlot';
|
|
|
|
const NoData = ({ message = 'No data available' }: { message?: string }) => (
|
|
<div className="flex items-center justify-center h-64 text-dark-500">
|
|
<div className="flex flex-col items-center gap-2">
|
|
<BarChart3 className="w-8 h-8" />
|
|
<span className="text-sm">{message}</span>
|
|
</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 [error, setError] = useState<string | null>(null);
|
|
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);
|
|
setError(null);
|
|
try {
|
|
// Load trial history (required)
|
|
const historyRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/history?limit=500`);
|
|
if (!historyRes.ok) {
|
|
throw new Error(`Failed to load trial history: ${historyRes.statusText}`);
|
|
}
|
|
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 (optional, graceful fallback)
|
|
try {
|
|
const metadataRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/metadata`);
|
|
if (metadataRes.ok) {
|
|
const metadataData = await metadataRes.json();
|
|
setMetadata(metadataData);
|
|
}
|
|
} catch {
|
|
console.warn('Metadata not available');
|
|
}
|
|
|
|
// Load Pareto front (optional)
|
|
try {
|
|
const paretoRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/pareto-front`);
|
|
if (paretoRes.ok) {
|
|
const paretoData = await paretoRes.json();
|
|
if (paretoData.is_multi_objective && paretoData.pareto_front) {
|
|
setParetoFront(paretoData.pareto_front);
|
|
}
|
|
}
|
|
} catch {
|
|
console.warn('Pareto data not available');
|
|
}
|
|
|
|
// Load runs data for comparison (optional)
|
|
try {
|
|
const runsRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/runs`);
|
|
if (runsRes.ok) {
|
|
const runsData = await runsRes.json();
|
|
if (runsData.runs) {
|
|
setRuns(runsData.runs);
|
|
}
|
|
}
|
|
} catch {
|
|
console.warn('Runs data not available');
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load analysis data:', err);
|
|
setError(err instanceof Error ? err.message : 'Failed to load analysis data');
|
|
} 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">
|
|
{/* 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>
|
|
) : error ? (
|
|
<div className="flex flex-col items-center justify-center py-16 gap-4">
|
|
<div className="text-red-400 text-6xl">!</div>
|
|
<div className="text-red-400 text-lg font-medium">Error Loading Data</div>
|
|
<div className="text-dark-400 text-sm max-w-md text-center">{error}</div>
|
|
<button
|
|
onClick={() => window.location.reload()}
|
|
className="mt-4 px-4 py-2 bg-primary-600 hover:bg-primary-500 text-white rounded-lg transition-colors"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
) : trials.length === 0 ? (
|
|
<NoData message="No trials found for this study" />
|
|
) : (
|
|
<>
|
|
{/* 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">
|
|
<ConvergencePlot
|
|
trials={trials}
|
|
objectiveIndex={0}
|
|
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
|
|
direction="minimize"
|
|
/>
|
|
</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">
|
|
<ParameterImportanceChart
|
|
trials={trials}
|
|
designVariables={metadata.design_variables}
|
|
objectiveIndex={0}
|
|
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
|
|
/>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Parallel Coordinates */}
|
|
{trials.length > 0 && metadata && (
|
|
<Card title="Parallel Coordinates">
|
|
<ParallelCoordinatesPlot
|
|
paretoData={trials}
|
|
objectives={metadata.objectives || []}
|
|
designVariables={metadata.design_variables || []}
|
|
paretoFront={paretoFront}
|
|
/>
|
|
</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">
|
|
<ParetoPlot
|
|
paretoData={paretoFront}
|
|
objectives={metadata?.objectives || []}
|
|
allTrials={trials}
|
|
/>
|
|
</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 Analysis */}
|
|
{trials.length > 2 && (
|
|
<Card title="Parameter-Objective Correlation Analysis">
|
|
<CorrelationTable trials={trials} objectiveName={metadata?.objectives?.[0]?.name || 'Objective'} />
|
|
</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 Summary */}
|
|
<Card title="Feasibility Analysis">
|
|
<div className="p-4">
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<div className="flex-1 bg-dark-700 rounded-full h-4 overflow-hidden">
|
|
<div
|
|
className="h-full bg-green-500 transition-all duration-500"
|
|
style={{ width: `${stats.feasibilityRate}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-lg font-bold text-green-400">{stats.feasibilityRate.toFixed(1)}%</span>
|
|
</div>
|
|
<p className="text-dark-400 text-sm">
|
|
{stats.feasible} of {stats.total} trials satisfy all constraints
|
|
</p>
|
|
</div>
|
|
</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 Performance Summary */}
|
|
<Card title="Surrogate Model Performance">
|
|
<div className="grid grid-cols-2 gap-6 p-4">
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-dark-300 mb-3">Trial Distribution</h4>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
|
|
<span className="text-dark-200">FEA: {stats.feaTrials} trials</span>
|
|
<span className="text-dark-400 ml-auto">
|
|
{((stats.feaTrials / stats.total) * 100).toFixed(0)}%
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-3 h-3 bg-purple-500 rounded-full"></div>
|
|
<span className="text-dark-200">NN: {stats.nnTrials} trials</span>
|
|
<span className="text-dark-400 ml-auto">
|
|
{((stats.nnTrials / stats.total) * 100).toFixed(0)}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-dark-300 mb-3">Efficiency Gains</h4>
|
|
<div className="text-center p-4 bg-dark-750 rounded-lg">
|
|
<div className="text-3xl font-bold text-primary-400">
|
|
{stats.feaTrials > 0 ? `${(stats.total / stats.feaTrials).toFixed(1)}x` : '1.0x'}
|
|
</div>
|
|
<div className="text-xs text-dark-400 mt-1">Effective Speedup</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
<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</th>
|
|
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
|
|
<th className="text-left py-2 px-3 text-dark-400 font-medium">Trials</th>
|
|
<th className="text-left py-2 px-3 text-dark-400 font-medium">Best Value</th>
|
|
<th className="text-left py-2 px-3 text-dark-400 font-medium">Avg Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{runs.map((run) => (
|
|
<tr key={run.run_id} className="border-b border-dark-700">
|
|
<td className="py-2 px-3 font-mono text-white">{run.name || `Run ${run.run_id}`}</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-dark-200">{run.trial_count}</td>
|
|
<td className="py-2 px-3 font-mono text-green-400">{run.best_value?.toExponential(4) || 'N/A'}</td>
|
|
<td className="py-2 px-3 font-mono text-dark-300">{run.avg_value?.toExponential(4) || 'N/A'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</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>
|
|
);
|
|
}
|