feat: Add Zernike wavefront viewer and V14 TPE optimization study

Dashboard Zernike Analysis:
- Add ZernikeViewer component with tabbed UI (40°, 60°, 90° vs 20°)
- Generate 3D surface mesh plots with Mesh3d triangulation
- Full 50-mode Zernike coefficient tables with mode names
- Manufacturing metrics for 90_vs_20 (optician workload analysis)
- OP2 availability filter for FEA trials only
- Fix duplicate trial display with unique React keys
- Tab switching with proper event propagation

Backend API Enhancements:
- GET /studies/{id}/trials/{num}/zernike - Generate Zernike HTML on-demand
- GET /studies/{id}/zernike-available - List trials with OP2 files
- compute_manufacturing_metrics() for aberration analysis
- compute_rms_filter_j1to3() for optician workload metric

M1 Mirror V14 Study:
- TPE (Tree-structured Parzen Estimator) optimization
- Seeds from 496 prior FEA trials (V11+V12+V13)
- Weighted-sum objective: 5*obj_40 + 5*obj_60 + 1*obj_mfg
- Multivariate TPE with constant_liar for efficient exploration
- Ready for 8-hour overnight runs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Antoine
2025-12-10 21:34:07 -05:00
parent 96b196de58
commit 48404fd743
5 changed files with 1785 additions and 13 deletions

View File

@@ -16,6 +16,7 @@ import { ParameterImportanceChart } from '../components/ParameterImportanceChart
import { ConvergencePlot } from '../components/ConvergencePlot';
import { StudyReportViewer } from '../components/StudyReportViewer';
import { ConsoleOutput } from '../components/ConsoleOutput';
import { ZernikeButton } from '../components/ZernikeViewer';
import { ExpandableChart } from '../components/ExpandableChart';
import { CurrentTrialPanel, OptimizerStatePanel } from '../components/tracker';
import type { Trial } from '../types';
@@ -49,6 +50,8 @@ export default function Dashboard() {
const [sortBy, setSortBy] = useState<'performance' | 'chronological'>('performance');
const [trialsPage, setTrialsPage] = useState(0);
const trialsPerPage = 50; // Limit trials per page for performance
const [showOnlyFEA, setShowOnlyFEA] = useState(false); // Filter to show only trials with OP2 results
const [zernikeAvailableTrials, setZernikeAvailableTrials] = useState<Set<number>>(new Set()); // Trials with OP2 files
// Parameter Space axis selection (reserved for future use)
const [_paramXIndex, _setParamXIndex] = useState(0);
@@ -98,7 +101,12 @@ export default function Dashboard() {
onMessage: (msg) => {
if (msg.type === 'trial_completed') {
const trial = msg.data as Trial;
setAllTrials(prev => [...prev, trial]);
// Avoid duplicates by checking if trial already exists
setAllTrials(prev => {
const exists = prev.some(t => t.trial_number === trial.trial_number);
if (exists) return prev;
return [...prev, trial];
});
if (trial.objective !== null && trial.objective !== undefined && trial.objective < bestValue) {
const improvement = previousBestRef.current !== Infinity
? ((previousBestRef.current - trial.objective) / Math.abs(previousBestRef.current)) * 100
@@ -199,6 +207,18 @@ export default function Dashboard() {
setIsRunning(data.is_running);
})
.catch(err => console.error('Failed to load process status:', err));
// Fetch available Zernike trials (for mirror/zernike studies)
if (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) {
fetch(`/api/optimization/studies/${selectedStudyId}/zernike-available`)
.then(res => res.json())
.then(data => {
setZernikeAvailableTrials(new Set(data.available_trials || []));
})
.catch(err => console.error('Failed to load Zernike available trials:', err));
} else {
setZernikeAvailableTrials(new Set());
}
}
}, [selectedStudyId]);
@@ -217,22 +237,30 @@ export default function Dashboard() {
return () => clearInterval(pollStatus);
}, [selectedStudyId]);
// Sort trials based on selected sort order
// Sort and filter trials based on selected options
useEffect(() => {
let sorted = [...allTrials];
let filtered = [...allTrials];
// Filter to trials with OP2 results (for Zernike analysis)
if (showOnlyFEA && zernikeAvailableTrials.size > 0) {
filtered = filtered.filter(t => zernikeAvailableTrials.has(t.trial_number));
}
// Sort
if (sortBy === 'performance') {
// Sort by objective (best first)
sorted.sort((a, b) => {
filtered.sort((a, b) => {
const aObj = a.objective ?? Infinity;
const bObj = b.objective ?? Infinity;
return aObj - bObj;
});
} else {
// Chronological (newest first)
sorted.sort((a, b) => b.trial_number - a.trial_number);
filtered.sort((a, b) => b.trial_number - a.trial_number);
}
setDisplayedTrials(sorted);
}, [allTrials, sortBy]);
setDisplayedTrials(filtered);
setTrialsPage(0); // Reset pagination when filter changes
}, [allTrials, sortBy, showOnlyFEA, zernikeAvailableTrials]);
// Auto-refresh polling for trial history
// PERFORMANCE: Use limit and longer interval for large studies
@@ -649,8 +677,22 @@ export default function Dashboard() {
<Card
title={
<div className="flex items-center justify-between w-full">
<span>Trial History ({displayedTrials.length} trials)</span>
<span>Trial History ({displayedTrials.length}{showOnlyFEA ? ' with OP2' : ''} trials{showOnlyFEA && allTrials.length !== displayedTrials.length ? ` of ${allTrials.length}` : ''})</span>
<div className="flex gap-2 items-center">
{/* OP2/Zernike toggle - only show for mirror/zernike studies */}
{selectedStudyId && (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) && zernikeAvailableTrials.size > 0 && (
<button
onClick={() => setShowOnlyFEA(!showOnlyFEA)}
className={`px-3 py-1 rounded text-sm ${
showOnlyFEA
? 'bg-green-600 text-white'
: 'bg-dark-500 text-dark-200 hover:bg-dark-400'
}`}
title={`Show only ${zernikeAvailableTrials.size} trials with OP2 results (for Zernike analysis)`}
>
OP2 Only ({zernikeAvailableTrials.size})
</button>
)}
<button
onClick={() => setSortBy('performance')}
className={`px-3 py-1 rounded text-sm ${
@@ -699,13 +741,13 @@ export default function Dashboard() {
>
<div className="space-y-2 max-h-[600px] overflow-y-auto">
{displayedTrials.length > 0 ? (
displayedTrials.slice(trialsPage * trialsPerPage, (trialsPage + 1) * trialsPerPage).map(trial => {
displayedTrials.slice(trialsPage * trialsPerPage, (trialsPage + 1) * trialsPerPage).map((trial, idx) => {
const isExpanded = expandedTrials.has(trial.trial_number);
const isBest = trial.objective === bestValue;
return (
<div
key={trial.trial_number}
key={`trial-${trial.trial_number}-${idx}`}
className={`rounded-lg transition-all duration-200 cursor-pointer ${
isBest
? 'bg-green-900 border-l-4 border-green-400'
@@ -728,6 +770,14 @@ export default function Dashboard() {
? trial.objective.toFixed(4)
: 'N/A'}
</span>
{/* Zernike viewer button - only show for mirror/Zernike studies */}
{selectedStudyId && (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) && (
<ZernikeButton
studyId={selectedStudyId}
trialNumber={trial.trial_number}
compact
/>
)}
<span className="text-dark-400 text-sm">
{isExpanded ? '▼' : '▶'}
</span>
@@ -829,6 +879,16 @@ export default function Dashboard() {
</div>
</div>
)}
{/* Zernike Analysis Button - Full size in expanded view */}
{selectedStudyId && (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) && (
<div className="border-t border-dark-400 pt-3 mt-3">
<ZernikeButton
studyId={selectedStudyId}
trialNumber={trial.trial_number}
/>
</div>
)}
</div>
)}
</div>