## Protocol 13: Adaptive Multi-Objective Optimization - Iterative FEA + Neural Network surrogate workflow - Initial FEA sampling, NN training, NN-accelerated search - FEA validation of top NN predictions, retraining loop - adaptive_state.json tracks iteration history and best values - M1 mirror study (V11) with 103 FEA, 3000 NN trials ## Dashboard Visualization Enhancements - Added Plotly.js interactive charts (parallel coords, Pareto, convergence) - Lazy loading with React.lazy() for performance - Code splitting: plotly.js-basic-dist (~1MB vs 3.5MB) - Chart library toggle (Recharts default, Plotly on-demand) - ExpandableChart component for full-screen modal views - ConsoleOutput component for real-time log viewing ## Documentation - Protocol 13 detailed documentation - Dashboard visualization guide - Plotly components README - Updated run-optimization skill with Mode 5 (adaptive) ## Bug Fixes - Fixed TypeScript errors in dashboard components - Fixed Card component to accept ReactNode title - Removed unused imports across components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
10 KiB
Zernike Mirror Optimization Protocol
Overview
This document captures the learnings from the M1 mirror Zernike optimization studies (V1-V9), including the Assembly FEM (AFEM) workflow, subcase handling, and wavefront error metrics.
Assembly FEM (AFEM) Structure
NX File Organization
A typical telescope mirror assembly in NX consists of:
ASSY_M1.prt # Master assembly part
ASSY_M1_assyfem1.afm # Assembly FEM container
ASSY_M1_assyfem1_sim1.sim # Simulation file (this is what we solve)
M1_Blank.prt # Mirror blank part
M1_Blank_fem1.fem # Mirror blank mesh
M1_Blank_fem1_i.prt # Idealized geometry for FEM
M1_Vertical_Support_Skeleton.prt # Support structure part
M1_Vertical_Support_Skeleton_fem1.fem
M1_Vertical_Support_Skeleton_fem1_i.prt
Key Relationships
- Assembly Part (.prt) - Contains the CAD geometry and expressions (design parameters)
- Assembly FEM (.afm) - Links component FEMs together, defines connections
- Simulation (.sim) - Contains solutions, loads, boundary conditions, subcases
- Component FEMs (.fem) - Individual meshes that get assembled
Expression Propagation
Expressions defined in the master .prt propagate through the assembly:
- Modify expression in
ASSY_M1.prt - AFEM updates mesh connections automatically
- Solve via
.simfile
Multi-Subcase Analysis
Telescope Gravity Orientations
For telescope mirrors, we analyze multiple gravity orientations (subcases):
| Subcase | Elevation Angle | Purpose |
|---|---|---|
| 1 | 90 deg (zenith) | Polishing orientation - manufacturing reference |
| 2 | 20 deg | Low elevation - reference for relative metrics |
| 3 | 40 deg | Mid-low elevation |
| 4 | 60 deg | Mid-high elevation |
Subcase Mapping
Important: NX subcase numbers don't always match angle labels!
"subcase_labels": {
"1": "90deg", // Subcase 1 = 90 degrees
"2": "20deg", // Subcase 2 = 20 degrees (reference)
"3": "40deg", // Subcase 3 = 40 degrees
"4": "60deg" // Subcase 4 = 60 degrees
}
Always verify subcase-to-angle mapping by checking the NX simulation setup.
Zernike Wavefront Error Analysis
Optical Convention
For mirror surface deformation to wavefront error:
WFE = 2 * surface_displacement (reflection doubles the path difference)
Unit conversion:
NM_PER_MM = 1e6 # 1 mm displacement = 1e6 nm WFE contribution
wfe_nm = 2.0 * displacement_mm * 1e6
Zernike Polynomial Indexing
We use Noll indexing (standard in optics):
| J | Name | (n,m) | Correctable? |
|---|---|---|---|
| 1 | Piston | (0,0) | Yes - alignment |
| 2 | Tilt X | (1,-1) | Yes - alignment |
| 3 | Tilt Y | (1,1) | Yes - alignment |
| 4 | Defocus | (2,0) | Yes - focus adjustment |
| 5 | Astigmatism 45 | (2,-2) | Partially |
| 6 | Astigmatism 0 | (2,2) | Partially |
| 7 | Coma X | (3,-1) | No |
| 8 | Coma Y | (3,1) | No |
| 9 | Trefoil X | (3,-3) | No |
| 10 | Trefoil Y | (3,3) | No |
| 11 | Spherical | (4,0) | No |
RMS Metrics
| Metric | Filter | Use Case |
|---|---|---|
global_rms_nm |
None | Total surface error |
filtered_rms_nm |
J1-J4 removed | Uncorrectable error (optimization target) |
rms_filter_j1to3 |
J1-J3 removed | Optician workload (keeps defocus) |
Relative Metrics
For gravity-induced deformation, we compute relative WFE:
WFE_relative = WFE_target_orientation - WFE_reference_orientation
This removes the static (manufacturing) shape and isolates gravity effects.
Example: rel_filtered_rms_40_vs_20 = filtered RMS at 40 deg relative to 20 deg reference
Optimization Objectives
Typical M1 Mirror Objectives
"objectives": [
{
"name": "rel_filtered_rms_40_vs_20",
"description": "Gravity-induced WFE at 40 deg vs 20 deg reference",
"direction": "minimize",
"weight": 5.0,
"target": 4.0,
"units": "nm"
},
{
"name": "rel_filtered_rms_60_vs_20",
"description": "Gravity-induced WFE at 60 deg vs 20 deg reference",
"direction": "minimize",
"weight": 5.0,
"target": 10.0,
"units": "nm"
},
{
"name": "mfg_90_optician_workload",
"description": "Polishing effort at zenith (J1-J3 filtered)",
"direction": "minimize",
"weight": 1.0,
"target": 20.0,
"units": "nm"
}
]
Weighted Sum Formulation
weighted_objective = sum(weight_i * (value_i / target_i)) / sum(weight_i)
Targets normalize different metrics to comparable scales.
Design Variables
Typical Mirror Support Parameters
| Parameter | Description | Typical Range |
|---|---|---|
whiffle_min |
Whiffle tree minimum dimension | 35-55 mm |
whiffle_outer_to_vertical |
Whiffle arm angle | 68-80 deg |
whiffle_triangle_closeness |
Triangle geometry | 50-65 mm |
inner_circular_rib_dia |
Rib diameter | 480-620 mm |
lateral_inner_angle |
Lateral support angle | 25-28.5 deg |
blank_backface_angle |
Mirror blank geometry | 3.5-5.0 deg |
Expression File Format (params.exp)
[mm]whiffle_min=42.49
[Degrees]whiffle_outer_to_vertical=79.41
[mm]inner_circular_rib_dia=582.48
Iteration Folder Structure (V9)
study_name/
├── 1_setup/
│ ├── model/ # Master NX files (NEVER modify)
│ └── optimization_config.json
├── 2_iterations/
│ ├── iter0/ # Trial 0 (0-based to match Optuna)
│ │ ├── [all NX files] # Fresh copy from master
│ │ ├── params.exp # Expression updates for this trial
│ │ └── results/ # Processed outputs
│ ├── iter1/
│ └── ...
└── 3_results/
└── study.db # Optuna database
Why 0-Based Iteration Folders?
Optuna uses 0-based trial numbers. Using iter{trial.number} ensures:
- Dashboard shows Trial 0 -> corresponds to folder iter0
- No confusion when cross-referencing results
- Consistent indexing throughout the system
Lessons Learned
1. TPE Sampler Seed Issue
Problem: When resuming a study, re-initializing TPESampler with a fixed seed causes the sampler to restart its random sequence, generating duplicate parameters.
Solution: Only set seed for NEW studies:
if is_new_study:
sampler = TPESampler(seed=42, ...)
else:
sampler = TPESampler(...) # No seed for resume
2. Code Reuse Protocol
Problem: Embedding 500+ lines of Zernike code in run_optimization.py violates DRY principle.
Solution: Use centralized extractors:
from optimization_engine.extractors import ZernikeExtractor
extractor = ZernikeExtractor(op2_file)
result = extractor.extract_relative("3", "2")
rms = result['relative_filtered_rms_nm']
3. Subcase Numbering
Problem: NX subcase numbers (1,2,3,4) don't match angle labels (20,40,60,90).
Solution: Use explicit mapping in config and translate:
subcase_labels = {"1": "90deg", "2": "20deg", "3": "40deg", "4": "60deg"}
label_to_subcase = {v: k for k, v in subcase_labels.items()}
4. OP2 Data Validation
Problem: Corrupt OP2 files can have all-zero or unrealistic displacement values.
Solution: Validate before processing:
unique_values = len(np.unique(disp_z))
if unique_values < 10:
raise RuntimeError("CORRUPT OP2: insufficient unique values")
if np.abs(disp_z).max() > 1e6:
raise RuntimeError("CORRUPT OP2: unrealistic displacement magnitude")
5. Reference Subcase for Relative Metrics
Problem: Which orientation to use as reference?
Solution: Use the lowest operational elevation (typically 20 deg) as reference. This makes higher elevations show positive relative WFE as gravity effects increase.
ZernikeExtractor API Reference
Basic Usage
from optimization_engine.extractors import ZernikeExtractor
# Create extractor
extractor = ZernikeExtractor(
op2_path="path/to/results.op2",
bdf_path=None, # Auto-detect from same folder
displacement_unit="mm",
n_modes=50,
filter_orders=4
)
# Single subcase
result = extractor.extract_subcase("2")
# Returns: global_rms_nm, filtered_rms_nm, rms_filter_j1to3, aberrations...
# Relative between subcases
rel = extractor.extract_relative(target_subcase="3", reference_subcase="2")
# Returns: relative_filtered_rms_nm, relative_rms_filter_j1to3, ...
# All subcases with relative metrics
all_results = extractor.extract_all_subcases(reference_subcase="2")
Available Metrics
| Method | Returns |
|---|---|
extract_subcase() |
global_rms_nm, filtered_rms_nm, rms_filter_j1to3, defocus_nm, astigmatism_rms_nm, coma_rms_nm, trefoil_rms_nm, spherical_nm |
extract_relative() |
relative_global_rms_nm, relative_filtered_rms_nm, relative_rms_filter_j1to3, relative aberrations |
extract_all_subcases() |
Dict of all subcases with both absolute and relative metrics |
Configuration Template
{
"study_name": "m1_mirror_optimization",
"design_variables": [
{
"name": "whiffle_min",
"expression_name": "whiffle_min",
"min": 35.0,
"max": 55.0,
"baseline": 40.55,
"units": "mm",
"enabled": true
}
],
"objectives": [
{
"name": "rel_filtered_rms_40_vs_20",
"extractor": "zernike_relative",
"extractor_config": {
"target_subcase": "3",
"reference_subcase": "2",
"metric": "relative_filtered_rms_nm"
},
"direction": "minimize",
"weight": 5.0,
"target": 4.0
}
],
"zernike_settings": {
"n_modes": 50,
"filter_low_orders": 4,
"displacement_unit": "mm",
"subcases": ["1", "2", "3", "4"],
"subcase_labels": {"1": "90deg", "2": "20deg", "3": "40deg", "4": "60deg"},
"reference_subcase": "2"
},
"optimization_settings": {
"sampler": "TPE",
"seed": 42,
"n_startup_trials": 15
}
}
Version History
| Version | Key Changes |
|---|---|
| V1-V6 | Initial development, various folder structures |
| V7 | HEEDS-style iteration folders, fresh model copies |
| V8 | Autonomous NX session management, but had embedded Zernike code |
| V9 | Clean ZernikeExtractor integration, fixed sampler seed, 0-based folders |