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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user