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:
Antoine
2025-12-05 19:57:20 -05:00
parent 5c660ff270
commit 5fb94fdf01
27 changed files with 5878 additions and 722 deletions

View File

@@ -0,0 +1,185 @@
import { useState, useEffect } from 'react';
import { Activity, Clock, Cpu, Zap, CheckCircle } from 'lucide-react';
interface CurrentTrialProps {
studyId: string | null;
totalTrials: number;
completedTrials: number;
isRunning: boolean;
lastTrialTime?: number; // ms for last trial
}
type TrialPhase = 'idle' | 'sampling' | 'evaluating' | 'extracting' | 'complete';
export function CurrentTrialPanel({
studyId,
totalTrials,
completedTrials,
isRunning,
lastTrialTime
}: CurrentTrialProps) {
const [elapsedTime, setElapsedTime] = useState(0);
const [phase, setPhase] = useState<TrialPhase>('idle');
// Simulate phase progression when running
useEffect(() => {
if (!isRunning) {
setPhase('idle');
setElapsedTime(0);
return;
}
setPhase('sampling');
const interval = setInterval(() => {
setElapsedTime(prev => {
const newTime = prev + 1;
// Simulate phase transitions based on typical timing
if (newTime < 2) setPhase('sampling');
else if (newTime < 5) setPhase('evaluating');
else setPhase('extracting');
return newTime;
});
}, 1000);
return () => clearInterval(interval);
}, [isRunning, completedTrials]);
// Reset elapsed time when a new trial completes
useEffect(() => {
if (isRunning) {
setElapsedTime(0);
setPhase('sampling');
}
}, [completedTrials, isRunning]);
// Calculate ETA
const calculateETA = () => {
if (!isRunning || completedTrials === 0 || !lastTrialTime) return null;
const remainingTrials = totalTrials - completedTrials;
const avgTimePerTrial = lastTrialTime / 1000; // convert to seconds
const etaSeconds = remainingTrials * avgTimePerTrial;
if (etaSeconds < 60) return `~${Math.round(etaSeconds)}s`;
if (etaSeconds < 3600) return `~${Math.round(etaSeconds / 60)}m`;
return `~${(etaSeconds / 3600).toFixed(1)}h`;
};
const progressPercent = totalTrials > 0 ? (completedTrials / totalTrials) * 100 : 0;
const eta = calculateETA();
const getPhaseInfo = () => {
switch (phase) {
case 'sampling':
return { label: 'Sampling', color: 'text-blue-400', bgColor: 'bg-blue-500/20', icon: Zap };
case 'evaluating':
return { label: 'FEA Solving', color: 'text-yellow-400', bgColor: 'bg-yellow-500/20', icon: Cpu };
case 'extracting':
return { label: 'Extracting', color: 'text-purple-400', bgColor: 'bg-purple-500/20', icon: Activity };
case 'complete':
return { label: 'Complete', color: 'text-green-400', bgColor: 'bg-green-500/20', icon: CheckCircle };
default:
return { label: 'Idle', color: 'text-dark-400', bgColor: 'bg-dark-600', icon: Clock };
}
};
const phaseInfo = getPhaseInfo();
const PhaseIcon = phaseInfo.icon;
if (!studyId) return null;
return (
<div className="bg-dark-750 rounded-lg border border-dark-600 p-4">
{/* Header Row */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Activity className={`w-5 h-5 ${isRunning ? 'text-green-400 animate-pulse' : 'text-dark-400'}`} />
<span className="font-semibold text-white">
{isRunning ? `Trial #${completedTrials + 1}` : 'Optimization Status'}
</span>
</div>
{isRunning && (
<span className={`flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium ${phaseInfo.bgColor} ${phaseInfo.color}`}>
<PhaseIcon className="w-3 h-3" />
{phaseInfo.label}
</span>
)}
</div>
{/* Progress Bar */}
<div className="mb-3">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-dark-400">Progress</span>
<span className="text-white font-medium">
{completedTrials} / {totalTrials} trials
</span>
</div>
<div className="h-2 bg-dark-600 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
isRunning ? 'bg-gradient-to-r from-primary-600 to-primary-400' : 'bg-primary-500'
}`}
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
{/* Stats Row */}
<div className="grid grid-cols-3 gap-3">
{/* Elapsed Time */}
<div className="text-center">
<div className={`text-lg font-mono ${isRunning ? 'text-white' : 'text-dark-400'}`}>
{isRunning ? `${elapsedTime}s` : '--'}
</div>
<div className="text-xs text-dark-400">Elapsed</div>
</div>
{/* Completion */}
<div className="text-center border-x border-dark-600">
<div className="text-lg font-mono text-primary-400">
{progressPercent.toFixed(1)}%
</div>
<div className="text-xs text-dark-400">Complete</div>
</div>
{/* ETA */}
<div className="text-center">
<div className={`text-lg font-mono ${eta ? 'text-blue-400' : 'text-dark-400'}`}>
{eta || '--'}
</div>
<div className="text-xs text-dark-400">ETA</div>
</div>
</div>
{/* Running indicator */}
{isRunning && (
<div className="mt-3 pt-3 border-t border-dark-600">
<div className="flex items-center justify-center gap-2 text-xs text-green-400">
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
Optimization in progress...
</div>
</div>
)}
{/* Paused/Stopped indicator */}
{!isRunning && completedTrials > 0 && completedTrials < totalTrials && (
<div className="mt-3 pt-3 border-t border-dark-600">
<div className="flex items-center justify-center gap-2 text-xs text-orange-400">
<span className="w-2 h-2 bg-orange-500 rounded-full" />
Optimization paused
</div>
</div>
)}
{/* Completed indicator */}
{!isRunning && completedTrials >= totalTrials && totalTrials > 0 && (
<div className="mt-3 pt-3 border-t border-dark-600">
<div className="flex items-center justify-center gap-2 text-xs text-blue-400">
<CheckCircle className="w-3 h-3" />
Optimization complete
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,158 @@
import { Cpu, Layers, Target, TrendingUp, Database, Brain } from 'lucide-react';
interface OptimizerStatePanelProps {
sampler?: string;
nTrials: number;
completedTrials: number;
feaTrials?: number;
nnTrials?: number;
objectives?: Array<{ name: string; direction: string }>;
isMultiObjective: boolean;
paretoSize?: number;
}
export function OptimizerStatePanel({
sampler = 'TPESampler',
nTrials,
completedTrials,
feaTrials = 0,
nnTrials = 0,
objectives = [],
isMultiObjective,
paretoSize = 0
}: OptimizerStatePanelProps) {
// Determine optimizer phase based on progress
const getPhase = () => {
if (completedTrials === 0) return 'Initializing';
if (completedTrials < 10) return 'Exploration';
if (completedTrials < nTrials * 0.5) return 'Exploitation';
if (completedTrials < nTrials * 0.9) return 'Refinement';
return 'Convergence';
};
const phase = getPhase();
// Format sampler name for display
const formatSampler = (s: string) => {
const samplers: Record<string, string> = {
'TPESampler': 'TPE (Bayesian)',
'NSGAIISampler': 'NSGA-II',
'NSGAIIISampler': 'NSGA-III',
'CmaEsSampler': 'CMA-ES',
'RandomSampler': 'Random',
'GridSampler': 'Grid',
'QMCSampler': 'Quasi-Monte Carlo'
};
return samplers[s] || s;
};
return (
<div className="bg-dark-750 rounded-lg border border-dark-600 p-4">
{/* Header */}
<div className="flex items-center gap-2 mb-4">
<Cpu className="w-5 h-5 text-primary-400" />
<span className="font-semibold text-white">Optimizer State</span>
</div>
{/* Main Stats Grid */}
<div className="grid grid-cols-2 gap-3 mb-4">
{/* Sampler */}
<div className="bg-dark-700 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Target className="w-4 h-4 text-dark-400" />
<span className="text-xs text-dark-400 uppercase">Sampler</span>
</div>
<div className="text-sm font-medium text-white truncate" title={sampler}>
{formatSampler(sampler)}
</div>
</div>
{/* Phase */}
<div className="bg-dark-700 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-4 h-4 text-dark-400" />
<span className="text-xs text-dark-400 uppercase">Phase</span>
</div>
<div className={`text-sm font-medium ${
phase === 'Convergence' ? 'text-green-400' :
phase === 'Refinement' ? 'text-blue-400' :
phase === 'Exploitation' ? 'text-yellow-400' :
'text-primary-400'
}`}>
{phase}
</div>
</div>
</div>
{/* FEA vs NN Trials (for hybrid optimizations) */}
{(feaTrials > 0 || nnTrials > 0) && (
<div className="mb-4">
<div className="text-xs text-dark-400 uppercase mb-2">Trial Sources</div>
<div className="flex gap-2">
<div className="flex-1 bg-dark-700 rounded-lg p-2 text-center">
<Database className="w-4 h-4 text-blue-400 mx-auto mb-1" />
<div className="text-lg font-bold text-blue-400">{feaTrials}</div>
<div className="text-xs text-dark-400">FEA</div>
</div>
<div className="flex-1 bg-dark-700 rounded-lg p-2 text-center">
<Brain className="w-4 h-4 text-purple-400 mx-auto mb-1" />
<div className="text-lg font-bold text-purple-400">{nnTrials}</div>
<div className="text-xs text-dark-400">Neural Net</div>
</div>
</div>
{nnTrials > 0 && (
<div className="mt-2 text-xs text-dark-400 text-center">
{((nnTrials / (feaTrials + nnTrials)) * 100).toFixed(0)}% acceleration from surrogate
</div>
)}
</div>
)}
{/* Objectives */}
{objectives.length > 0 && (
<div className="mb-4">
<div className="flex items-center gap-2 mb-2">
<Layers className="w-4 h-4 text-dark-400" />
<span className="text-xs text-dark-400 uppercase">
{isMultiObjective ? 'Multi-Objective' : 'Single Objective'}
</span>
</div>
<div className="space-y-1">
{objectives.slice(0, 3).map((obj, idx) => (
<div
key={idx}
className="flex items-center justify-between text-sm bg-dark-700 rounded px-2 py-1"
>
<span className="text-dark-300 truncate" title={obj.name}>
{obj.name.length > 20 ? obj.name.slice(0, 18) + '...' : obj.name}
</span>
<span className={`text-xs px-1.5 py-0.5 rounded ${
obj.direction === 'minimize' ? 'bg-green-900/50 text-green-400' : 'bg-blue-900/50 text-blue-400'
}`}>
{obj.direction === 'minimize' ? 'min' : 'max'}
</span>
</div>
))}
{objectives.length > 3 && (
<div className="text-xs text-dark-500 text-center">
+{objectives.length - 3} more
</div>
)}
</div>
</div>
)}
{/* Pareto Front Size (for multi-objective) */}
{isMultiObjective && paretoSize > 0 && (
<div className="pt-3 border-t border-dark-600">
<div className="flex items-center justify-between">
<span className="text-xs text-dark-400">Pareto Front Size</span>
<span className="text-sm font-medium text-primary-400">
{paretoSize} solutions
</span>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,2 @@
export { CurrentTrialPanel } from './CurrentTrialPanel';
export { OptimizerStatePanel } from './OptimizerStatePanel';