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>
365 lines
11 KiB
Markdown
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)
|