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 }) => (
{message}
); 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; user_attrs?: Record; 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('overview'); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [trials, setTrials] = useState([]); const [metadata, setMetadata] = useState(null); const [paretoFront, setParetoFront] = useState([]); const [runs, setRuns] = useState([]); // 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 (

Loading...

); } const isMultiObjective = (metadata?.objectives?.length || 0) > 1; return (
{/* Header */}

Analysis

Deep analysis for {selectedStudy.name || selectedStudy.id}

{/* Tab Navigation */}
{tabs.map(tab => ( ))}
{loading ? (
) : error ? (
!
Error Loading Data
{error}
) : trials.length === 0 ? ( ) : ( <> {/* Overview Tab */} {activeTab === 'overview' && (
{/* Summary Stats */} {stats && (
Total Trials
{stats.total}
Best Value
{stats.min.toExponential(3)}
Mean
{stats.mean.toExponential(3)}
Median
{stats.median.toExponential(3)}
Std Dev
{stats.stdDev.toExponential(3)}
Feasibility
{stats.feasibilityRate.toFixed(1)}%
)} {/* Percentile Distribution */} {stats && (
Min
{stats.min.toExponential(3)}
25th %
{stats.p25.toExponential(3)}
75th %
{stats.p75.toExponential(3)}
90th %
{stats.p90.toExponential(3)}
)} {/* Convergence Plot */} {trials.length > 0 && ( )} {/* Best Trials Table */}
{Object.keys(trials[0]?.params || {}).slice(0, 3).map(p => ( ))} {[...trials] .sort((a, b) => (a.values[0] ?? Infinity) - (b.values[0] ?? Infinity)) .slice(0, 10) .map((trial, idx) => ( {Object.keys(trials[0]?.params || {}).slice(0, 3).map(p => ( ))} ))}
Rank Trial Objective Source{p}
{idx + 1} #{trial.trial_number} {trial.values[0]?.toExponential(4)} {trial.source} {trial.params[p]?.toFixed(4)}
)} {/* Parameters Tab */} {activeTab === 'parameters' && (
{/* Parameter Importance */} {trials.length > 0 && metadata?.design_variables && ( )} {/* Parallel Coordinates */} {trials.length > 0 && metadata && ( )}
)} {/* Pareto Tab */} {activeTab === 'pareto' && isMultiObjective && (
{/* Pareto Metrics */}
Pareto Solutions
{paretoFront.length}
Objectives
{metadata?.objectives?.length || 0}
Dominated Ratio
{trials.length > 0 ? ((1 - paretoFront.length / trials.length) * 100).toFixed(1) : 0}%
{/* Pareto Front Plot */} {paretoFront.length > 0 && ( )} {/* Pareto Solutions Table */}
{metadata?.objectives?.map(obj => ( ))} {paretoFront.slice(0, 20).map((sol, idx) => ( {sol.values?.map((v: number, i: number) => ( ))} ))}
Trial{obj.name}
#{sol.trial_number}{v?.toExponential(4)}
)} {/* Correlations Tab */} {activeTab === 'correlations' && (
{/* Correlation Analysis */} {trials.length > 2 && ( )} {/* Correlation Interpretation Guide */}
Strong Positive (0.7 to 1.0)

Increasing parameter increases objective

Moderate Positive (0.3 to 0.7)

Some positive relationship

Moderate Negative (-0.7 to -0.3)

Some negative relationship

Strong Negative (-1.0 to -0.7)

Increasing parameter decreases objective

{/* Top Correlations Table */} {trials.length > 2 && ( )}
)} {/* Constraints Tab */} {activeTab === 'constraints' && stats && (
Feasible Trials
{stats.feasible}
Infeasible Trials
{stats.total - stats.feasible}
Feasibility Rate
{stats.feasibilityRate.toFixed(1)}%
{/* Feasibility Summary */}
{stats.feasibilityRate.toFixed(1)}%

{stats.feasible} of {stats.total} trials satisfy all constraints

{/* Infeasible Trials List */} {stats.total - stats.feasible > 0 && (
{trials .filter(t => !t.constraint_satisfied) .slice(-20) .reverse() .map(trial => ( ))}
Trial Objective Source
#{trial.trial_number} {trial.values[0]?.toExponential(4) || 'N/A'} {trial.source}
)}
)} {/* Surrogate Tab */} {activeTab === 'surrogate' && stats && (
FEA Evaluations
{stats.feaTrials}
NN Predictions
{stats.nnTrials}
NN Ratio
{stats.nnTrials > 0 ? `${((stats.nnTrials / stats.total) * 100).toFixed(0)}%` : '0%'}
Speedup Factor
{stats.feaTrials > 0 ? `${(stats.total / stats.feaTrials).toFixed(1)}x` : '1.0x'}
{/* Surrogate Performance Summary */}

Trial Distribution

FEA: {stats.feaTrials} trials {((stats.feaTrials / stats.total) * 100).toFixed(0)}%
NN: {stats.nnTrials} trials {((stats.nnTrials / stats.total) * 100).toFixed(0)}%

Efficiency Gains

{stats.feaTrials > 0 ? `${(stats.total / stats.feaTrials).toFixed(1)}x` : '1.0x'}
Effective Speedup
)} {/* Runs Tab */} {activeTab === 'runs' && runs.length > 0 && (

Compare different optimization runs within this study. Studies with adaptive optimization may have multiple runs (e.g., initial FEA exploration, NN-accelerated iterations).

{runs.map((run) => ( ))}
Run Source Trials Best Value Avg Value
{run.name || `Run ${run.run_id}`} {run.source} {run.trial_count} {run.best_value?.toExponential(4) || 'N/A'} {run.avg_value?.toExponential(4) || 'N/A'}
)} )}
); } // 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

Not enough data for correlation analysis

; } return ( {correlations.slice(0, 10).map(({ param, correlation, absCorr }) => ( ))}
Parameter Correlation with {objectiveName} Strength
{param}
0 ? 'bg-blue-500' : 'bg-red-500'}`} style={{ width: `${absCorr * 100}%`, marginLeft: correlation < 0 ? 'auto' : 0 }} />
0.7 ? 'text-white font-bold' : absCorr > 0.3 ? 'text-dark-200' : 'text-dark-400' }`}> {correlation > 0 ? '+' : ''}{correlation.toFixed(3)}
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'}
); }