Files
Atomizer/.claude/skills/modules/zernike-optimization.md
Antoine 602560c46a feat: Add MLP surrogate with Turbo Mode for 100x faster optimization
Neural Acceleration (MLP Surrogate):
- Add run_nn_optimization.py with hybrid FEA/NN workflow
- MLP architecture: 4-layer (64->128->128->64) with BatchNorm/Dropout
- Three workflow modes:
  - --all: Sequential export->train->optimize->validate
  - --hybrid-loop: Iterative Train->NN->Validate->Retrain cycle
  - --turbo: Aggressive single-best validation (RECOMMENDED)
- Turbo mode: 5000 NN trials + 50 FEA validations in ~12 minutes
- Separate nn_study.db to avoid overloading dashboard

Performance Results (bracket_pareto_3obj study):
- NN prediction errors: mass 1-5%, stress 1-4%, stiffness 5-15%
- Found minimum mass designs at boundary (angle~30deg, thick~30mm)
- 100x speedup vs pure FEA exploration

Protocol Operating System:
- Add .claude/skills/ with Bootstrap, Cheatsheet, Context Loader
- Add docs/protocols/ with operations (OP_01-06) and system (SYS_10-14)
- Update SYS_14_NEURAL_ACCELERATION.md with MLP Turbo Mode docs

NX Automation:
- Add optimization_engine/hooks/ for NX CAD/CAE automation
- Add study_wizard.py for guided study creation
- Fix FEM mesh update: load idealized part before UpdateFemodel()

New Study:
- bracket_pareto_3obj: 3-objective Pareto (mass, stress, stiffness)
- 167 FEA trials + 5000 NN trials completed
- Demonstrates full hybrid workflow

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 20:01:59 -05:00

365 lines
11 KiB
Markdown

# Zernike Optimization Module
**Last Updated**: December 5, 2025
**Version**: 1.0
**Type**: Optional Module
This module provides specialized guidance for telescope mirror and optical surface optimization using Zernike polynomial decomposition.
---
## When to Load
- User mentions "telescope", "mirror", "optical", "wavefront"
- Optimization involves surface deformation analysis
- Need to extract Zernike coefficients from FEA results
- Working with multi-subcase elevation angle comparisons
---
## Zernike Extractors (E8-E10)
| ID | Extractor | Function | Input | Output | Use Case |
|----|-----------|----------|-------|--------|----------|
| E8 | **Zernike WFE** | `extract_zernike_from_op2()` | `.op2` + `.bdf` | nm | Single subcase wavefront error |
| E9 | **Zernike Relative** | `extract_zernike_relative_rms()` | `.op2` + `.bdf` | nm | Compare target vs reference subcase |
| E10 | **Zernike Helpers** | `ZernikeObjectiveBuilder` | `.op2` | nm | Multi-subcase optimization builder |
---
## E8: Single Subcase Zernike Extraction
Extract Zernike coefficients and RMS metrics for a single subcase (e.g., one elevation angle).
```python
from optimization_engine.extractors.extract_zernike import extract_zernike_from_op2
# Extract Zernike coefficients and RMS metrics for a single subcase
result = extract_zernike_from_op2(
op2_file,
bdf_file=None, # Auto-detect from op2 location
subcase="20", # Subcase label (e.g., "20" = 20 deg elevation)
displacement_unit="mm"
)
global_rms = result['global_rms_nm'] # Total surface RMS in nm
filtered_rms = result['filtered_rms_nm'] # RMS with low orders removed
coefficients = result['coefficients'] # List of 50 Zernike coefficients
```
**Return Dictionary**:
```python
{
'global_rms_nm': 45.2, # Total surface RMS (nm)
'filtered_rms_nm': 12.8, # RMS with J1-J4 (piston, tip, tilt, defocus) removed
'coefficients': [0.0, 12.3, ...], # 50 Zernike coefficients (Noll indexing)
'n_nodes': 5432, # Number of surface nodes
'rms_per_mode': {...} # RMS contribution per Zernike mode
}
```
**When to Use**:
- Single elevation angle analysis
- Polishing orientation (zenith) wavefront error
- Absolute surface quality metrics
---
## E9: Relative RMS Between Subcases
Compare wavefront error between two subcases (e.g., 40° vs 20° reference).
```python
from optimization_engine.extractors.extract_zernike import extract_zernike_relative_rms
# Compare wavefront error between subcases (e.g., 40 deg vs 20 deg reference)
result = extract_zernike_relative_rms(
op2_file,
bdf_file=None,
target_subcase="40", # Target orientation
reference_subcase="20", # Reference (usually polishing orientation)
displacement_unit="mm"
)
relative_rms = result['relative_filtered_rms_nm'] # Differential WFE in nm
delta_coeffs = result['delta_coefficients'] # Coefficient differences
```
**Return Dictionary**:
```python
{
'relative_filtered_rms_nm': 8.7, # Differential WFE (target - reference)
'delta_coefficients': [...], # Coefficient differences
'target_rms_nm': 52.3, # Target subcase absolute RMS
'reference_rms_nm': 45.2, # Reference subcase absolute RMS
'improvement_percent': -15.7 # Negative = worse than reference
}
```
**When to Use**:
- Comparing performance across elevation angles
- Minimizing deformation relative to polishing orientation
- Multi-angle telescope mirror optimization
---
## E10: Multi-Subcase Objective Builder
Build objectives for multiple subcases in a single extractor (most efficient for complex optimization).
```python
from optimization_engine.extractors.zernike_helpers import ZernikeObjectiveBuilder
# Build objectives for multiple subcases in one extractor
builder = ZernikeObjectiveBuilder(
op2_finder=lambda: model_dir / "ASSY_M1-solution_1.op2"
)
# Add relative objectives (target vs reference)
builder.add_relative_objective(
"40", "20", # 40° vs 20° reference
metric="relative_filtered_rms_nm",
weight=5.0
)
builder.add_relative_objective(
"60", "20", # 60° vs 20° reference
metric="relative_filtered_rms_nm",
weight=5.0
)
# Add absolute objective for polishing orientation
builder.add_subcase_objective(
"90", # Zenith (polishing orientation)
metric="rms_filter_j1to3", # Only remove piston, tip, tilt
weight=1.0
)
# Evaluate all at once (efficient - parses OP2 only once)
results = builder.evaluate_all()
# Returns: {'rel_40_vs_20': 4.2, 'rel_60_vs_20': 8.7, 'rms_90': 15.3}
```
**When to Use**:
- Multi-objective telescope optimization
- Multiple elevation angles to optimize
- Weighted combination of absolute and relative WFE
---
## Zernike Modes Reference
| Noll Index | Name | Physical Meaning | Correctability |
|------------|------|------------------|----------------|
| J1 | Piston | Constant offset | Easily corrected |
| J2 | Tip | X-tilt | Easily corrected |
| J3 | Tilt | Y-tilt | Easily corrected |
| J4 | Defocus | Power error | Easily corrected |
| J5 | Astigmatism (0°) | Cylindrical error | Correctable |
| J6 | Astigmatism (45°) | Cylindrical error | Correctable |
| J7 | Coma (x) | Off-axis aberration | Harder to correct |
| J8 | Coma (y) | Off-axis aberration | Harder to correct |
| J9-J10 | Trefoil | Triangular error | Hard to correct |
| J11+ | Higher order | Complex aberrations | Very hard to correct |
**Filtering Convention**:
- `filtered_rms`: Removes J1-J4 (piston, tip, tilt, defocus) - standard
- `rms_filter_j1to3`: Removes only J1-J3 (keeps defocus) - for focus-sensitive applications
---
## Common Zernike Optimization Patterns
### Pattern 1: Minimize Relative WFE Across Elevations
```python
# Objective: Minimize max relative WFE across all elevation angles
objectives = [
{"name": "rel_40_vs_20", "goal": "minimize"},
{"name": "rel_60_vs_20", "goal": "minimize"},
]
# Use weighted sum or multi-objective
def objective(trial):
results = builder.evaluate_all()
return (results['rel_40_vs_20'], results['rel_60_vs_20'])
```
### Pattern 2: Single Elevation + Mass
```python
# Objective: Minimize WFE at 45° while minimizing mass
objectives = [
{"name": "wfe_45", "goal": "minimize"}, # Wavefront error
{"name": "mass", "goal": "minimize"}, # Mirror mass
]
```
### Pattern 3: Weighted Multi-Angle
```python
# Weighted combination of multiple angles
def combined_wfe(trial):
results = builder.evaluate_all()
weighted_wfe = (
5.0 * results['rel_40_vs_20'] +
5.0 * results['rel_60_vs_20'] +
1.0 * results['rms_90']
)
return weighted_wfe
```
---
## Telescope Mirror Study Configuration
```json
{
"study_name": "m1_mirror_optimization",
"description": "Minimize wavefront error across elevation angles",
"objectives": [
{
"name": "wfe_40_vs_20",
"goal": "minimize",
"unit": "nm",
"extraction": {
"action": "extract_zernike_relative_rms",
"params": {
"target_subcase": "40",
"reference_subcase": "20"
}
}
}
],
"simulation": {
"analysis_types": ["static"],
"subcases": ["20", "40", "60", "90"],
"solution_name": null
}
}
```
---
## Performance Considerations
1. **Parse OP2 Once**: Use `ZernikeObjectiveBuilder` to parse the OP2 file only once per trial
2. **Subcase Labels**: Match exact subcase labels from NX simulation
3. **Node Selection**: Zernike extraction uses surface nodes only (auto-detected from BDF)
4. **Memory**: Large meshes (>50k nodes) may require chunked processing
---
## Troubleshooting
| Symptom | Cause | Solution |
|---------|-------|----------|
| "Subcase not found" | Wrong subcase label | Check NX .sim for exact labels |
| High J1-J4 coefficients | Rigid body motion not constrained | Check boundary conditions |
| NaN in coefficients | Insufficient nodes for polynomial order | Reduce max Zernike order |
| Inconsistent RMS | Different node sets per subcase | Verify mesh consistency |
| "Billion nm" RMS values | Node merge failed in AFEM | Check `MergeOccurrenceNodes = True` |
| Corrupt OP2 data | All-zero displacements | Validate OP2 before processing |
---
## Assembly FEM (AFEM) Structure for Mirrors
Telescope mirror assemblies in NX typically consist of:
```
ASSY_M1.prt # Master assembly part
ASSY_M1_assyfem1.afm # Assembly FEM container
ASSY_M1_assyfem1_sim1.sim # Simulation file (solve this)
M1_Blank.prt # Mirror blank part
M1_Blank_fem1.fem # Mirror blank mesh
M1_Vertical_Support_Skeleton.prt # Support structure
```
**Key Point**: Expressions in master `.prt` propagate through assembly → AFEM updates automatically.
---
## Multi-Subcase Gravity Analysis
For telescope mirrors, analyze multiple gravity orientations:
| Subcase | Elevation Angle | Purpose |
|---------|-----------------|---------|
| 1 | 90° (zenith) | Polishing orientation - manufacturing reference |
| 2 | 20° | Low elevation - reference for relative metrics |
| 3 | 40° | Mid-low elevation |
| 4 | 60° | Mid-high elevation |
**CRITICAL**: NX subcase numbers don't always match angle labels! Use explicit mapping:
```json
"subcase_labels": {
"1": "90deg",
"2": "20deg",
"3": "40deg",
"4": "60deg"
}
```
---
## Lessons Learned (M1 Mirror V1-V9)
### 1. TPE Sampler Seed Issue
**Problem**: Resuming study with fixed seed causes duplicate parameters.
**Solution**:
```python
if is_new_study:
sampler = TPESampler(seed=42)
else:
sampler = TPESampler() # No seed for resume
```
### 2. OP2 Data Validation
**Always validate before processing**:
```python
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")
```
### 3. Reference Subcase Selection
Use lowest operational elevation (typically 20°) as reference. Higher elevations show positive relative WFE as gravity effects increase.
### 4. Optical Convention
For mirror surface to wavefront error:
```python
WFE = 2 * surface_displacement # Reflection doubles path difference
wfe_nm = 2.0 * displacement_mm * 1e6 # Convert mm to nm
```
---
## Typical Mirror Design Variables
| Parameter | Description | Typical Range |
|-----------|-------------|---------------|
| `whiffle_min` | Whiffle tree minimum dimension | 35-55 mm |
| `whiffle_outer_to_vertical` | Whiffle arm angle | 68-80 deg |
| `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 |
---
## Cross-References
- **Extractor Catalog**: [extractors-catalog module](./extractors-catalog.md)
- **System Protocol**: [SYS_12_EXTRACTOR_LIBRARY](../../docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md)
- **Core Skill**: [study-creation-core](../core/study-creation-core.md)