Phase 2 of restructuring plan: - Rename SYS_16_STUDY_INSIGHTS -> SYS_17_STUDY_INSIGHTS - Rename SYS_17_CONTEXT_ENGINEERING -> SYS_18_CONTEXT_ENGINEERING - Promote Bootstrap V3.0 (Context Engineering) as default - Archive old Bootstrap V2.0 - Create knowledge_base/playbook.json for ACE framework - Add OP_08 (Generate Report) to routing tables - Add SYS_16-18 to protocol tables - Update docs/protocols/README.md to version 1.1 - Update CLAUDE.md with new protocols - Create docs/plans/RESTRUCTURING_PLAN.md for continuation Remaining: Phase 2.8 (Cheatsheet), Phases 3-6 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
334 lines
8.9 KiB
Markdown
334 lines
8.9 KiB
Markdown
# Zernike Wavefront Analysis Integration
|
|
|
|
This document describes how to use Atomizer's Zernike analysis capabilities for telescope mirror optimization.
|
|
|
|
## Overview
|
|
|
|
Atomizer includes a full Zernike polynomial decomposition system for analyzing wavefront errors (WFE) in telescope mirror FEA simulations. The system:
|
|
|
|
- Extracts nodal displacements from NX Nastran OP2 files
|
|
- Fits Zernike polynomials using Noll indexing (optical standard)
|
|
- Computes RMS metrics (global and filtered)
|
|
- Analyzes individual aberrations (astigmatism, coma, trefoil, etc.)
|
|
- Supports multi-subcase analysis (different gravity orientations)
|
|
|
|
## Quick Start
|
|
|
|
### Simple Extraction
|
|
|
|
```python
|
|
from optimization_engine.extractors import extract_zernike_from_op2
|
|
|
|
# Extract Zernike metrics for a single subcase
|
|
result = extract_zernike_from_op2(
|
|
op2_file="model-solution_1.op2",
|
|
subcase="20" # 20 degree elevation
|
|
)
|
|
|
|
print(f"Global RMS: {result['global_rms_nm']:.2f} nm")
|
|
print(f"Filtered RMS: {result['filtered_rms_nm']:.2f} nm")
|
|
print(f"Astigmatism: {result['astigmatism_rms_nm']:.2f} nm")
|
|
```
|
|
|
|
### In Optimization Objective
|
|
|
|
```python
|
|
from optimization_engine.extractors.zernike_helpers import create_zernike_objective
|
|
|
|
# Create objective function
|
|
zernike_obj = create_zernike_objective(
|
|
op2_finder=lambda: sim_dir / "model-solution_1.op2",
|
|
subcase="20",
|
|
metric="filtered_rms_nm"
|
|
)
|
|
|
|
# Use in Optuna trial
|
|
def objective(trial):
|
|
# ... suggest parameters ...
|
|
# ... run simulation ...
|
|
|
|
rms = zernike_obj()
|
|
return rms
|
|
```
|
|
|
|
## RMS Calculation Method
|
|
|
|
**IMPORTANT**: Atomizer uses the correct surface-based RMS calculation matching optical standards:
|
|
|
|
```python
|
|
# Global RMS = sqrt(mean(W^2)) - RMS of actual WFE surface values
|
|
global_rms = sqrt(mean(W_nm ** 2))
|
|
|
|
# Filtered RMS = sqrt(mean(W_residual^2))
|
|
# where W_residual = W_nm - Z[:, :4] @ coeffs[:4] (low-order fit subtracted)
|
|
filtered_rms = sqrt(mean(W_residual ** 2))
|
|
```
|
|
|
|
This is **different** from summing Zernike coefficients! The RMS is computed from the actual WFE surface values, not from `sqrt(sum(coeffs^2))`.
|
|
|
|
## Available Metrics
|
|
|
|
### RMS Metrics
|
|
| Metric | Description |
|
|
|--------|-------------|
|
|
| `global_rms_nm` | RMS of entire WFE surface: `sqrt(mean(W^2))` |
|
|
| `filtered_rms_nm` | RMS after removing modes 1-4 (piston, tip, tilt, defocus) |
|
|
| `rms_filter_j1to3_nm` | RMS after removing only modes 1-3 (keeps defocus) - "optician workload" |
|
|
|
|
### Aberration Magnitudes
|
|
| Metric | Zernike Modes | Description |
|
|
|--------|--------------|-------------|
|
|
| `defocus_nm` | J4 | Focus error |
|
|
| `astigmatism_rms_nm` | J5 + J6 | Combined astigmatism |
|
|
| `coma_rms_nm` | J7 + J8 | Combined coma |
|
|
| `trefoil_rms_nm` | J9 + J10 | Combined trefoil |
|
|
| `spherical_nm` | J11 | Primary spherical |
|
|
|
|
## Multi-Subcase Analysis
|
|
|
|
For telescope mirrors, gravity orientation affects surface shape. Standard subcases:
|
|
|
|
| Subcase | Description |
|
|
|---------|-------------|
|
|
| 20 | Low elevation (operational) |
|
|
| 40 | Mid-low elevation |
|
|
| 60 | Mid-high elevation |
|
|
| 90 | Horizontal (polishing orientation) |
|
|
|
|
### Extract All Subcases
|
|
|
|
```python
|
|
from optimization_engine.extractors import ZernikeExtractor
|
|
|
|
extractor = ZernikeExtractor("model.op2")
|
|
results = extractor.extract_all_subcases(reference_subcase="20")
|
|
|
|
for label, metrics in results.items():
|
|
print(f"Subcase {label}: {metrics['filtered_rms_nm']:.1f} nm")
|
|
```
|
|
|
|
### Relative Analysis
|
|
|
|
Compare deformation between orientations:
|
|
|
|
```python
|
|
from optimization_engine.extractors.zernike_helpers import create_relative_zernike_objective
|
|
|
|
# Minimize deformation at 20 deg relative to polishing position (90 deg)
|
|
relative_obj = create_relative_zernike_objective(
|
|
op2_finder=lambda: sim_dir / "model.op2",
|
|
target_subcase="20",
|
|
reference_subcase="90"
|
|
)
|
|
|
|
relative_rms = relative_obj()
|
|
```
|
|
|
|
## Optimization Configuration
|
|
|
|
### Example: Single Objective (Filtered RMS)
|
|
|
|
```json
|
|
{
|
|
"objectives": [
|
|
{
|
|
"name": "filtered_rms",
|
|
"direction": "minimize",
|
|
"extractor": "zernike",
|
|
"extractor_config": {
|
|
"subcase": "20",
|
|
"metric": "filtered_rms_nm"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Example: Multi-Objective (RMS + Mass)
|
|
|
|
```json
|
|
{
|
|
"objectives": [
|
|
{
|
|
"name": "filtered_rms_20deg",
|
|
"direction": "minimize",
|
|
"extractor": "zernike",
|
|
"extractor_config": {
|
|
"subcase": "20",
|
|
"metric": "filtered_rms_nm"
|
|
}
|
|
},
|
|
{
|
|
"name": "mass",
|
|
"direction": "minimize",
|
|
"extractor": "mass_from_expression"
|
|
}
|
|
],
|
|
"optimization_settings": {
|
|
"sampler": "NSGA-II",
|
|
"protocol": 11
|
|
}
|
|
}
|
|
```
|
|
|
|
### Example: Constrained (Stress + Aberration Limits)
|
|
|
|
```json
|
|
{
|
|
"constraints": [
|
|
{
|
|
"name": "astigmatism_limit",
|
|
"type": "upper_bound",
|
|
"threshold": 50.0,
|
|
"extractor": "zernike",
|
|
"extractor_config": {
|
|
"subcase": "90",
|
|
"metric": "astigmatism_rms_nm"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Advanced: ZernikeObjectiveBuilder
|
|
|
|
For complex multi-subcase objectives:
|
|
|
|
```python
|
|
from optimization_engine.extractors.zernike_helpers import ZernikeObjectiveBuilder
|
|
|
|
builder = ZernikeObjectiveBuilder(
|
|
op2_finder=lambda: sim_dir / "model.op2"
|
|
)
|
|
|
|
# Weight operational positions more heavily
|
|
builder.add_subcase_objective("20", "filtered_rms_nm", weight=1.0)
|
|
builder.add_subcase_objective("40", "filtered_rms_nm", weight=0.5)
|
|
builder.add_subcase_objective("60", "filtered_rms_nm", weight=0.5)
|
|
|
|
# Create combined objective (weighted sum)
|
|
objective = builder.build_weighted_sum()
|
|
|
|
# Or: worst-case across subcases
|
|
worst_case_obj = builder.build_max()
|
|
```
|
|
|
|
## Zernike Settings
|
|
|
|
### Configuration Options
|
|
|
|
| Setting | Default | Description |
|
|
|---------|---------|-------------|
|
|
| `n_modes` | 50 | Number of Zernike modes to fit |
|
|
| `filter_orders` | 4 | Low-order modes to filter (1-4 = piston through defocus) |
|
|
| `displacement_unit` | "mm" | Unit of displacement in OP2 ("mm", "m", "um", "nm") |
|
|
|
|
### Unit Conversions
|
|
|
|
Wavefront error (WFE) is computed as:
|
|
|
|
```
|
|
WFE_nm = 2 * displacement * unit_conversion
|
|
```
|
|
|
|
Where `unit_conversion` converts to nanometers:
|
|
- mm: 1e6
|
|
- m: 1e9
|
|
- um: 1e3
|
|
|
|
The factor of 2 accounts for the optical convention (surface error doubles as wavefront error for reflection).
|
|
|
|
## NX Nastran Setup
|
|
|
|
### Required Subcases
|
|
|
|
Your NX Nastran model should have subcases for each gravity orientation:
|
|
|
|
```
|
|
SUBCASE 20
|
|
SUBTITLE=20 deg elevation
|
|
LOAD = ...
|
|
|
|
SUBCASE 40
|
|
SUBTITLE=40 deg elevation
|
|
LOAD = ...
|
|
```
|
|
|
|
The extractor identifies subcases by:
|
|
1. Numeric value in SUBTITLE (preferred)
|
|
2. SUBCASE ID number
|
|
|
|
### Output Requests
|
|
|
|
Ensure displacement output is requested:
|
|
|
|
```
|
|
SET 999 = ALL
|
|
DISPLACEMENT(SORT1,REAL) = 999
|
|
```
|
|
|
|
## Migration from Legacy Scripts
|
|
|
|
If you were using `zernike_Post_Script_NX.py`:
|
|
|
|
| Old Approach | Atomizer Equivalent |
|
|
|--------------|---------------------|
|
|
| Manual OP2 parsing | `ZernikeExtractor` |
|
|
| `compute_zernike_coeffs_chunked()` | `compute_zernike_coefficients()` |
|
|
| `write_exp_file()` | Configure as objective/constraint |
|
|
| HTML reports | Dashboard visualization (TBD) |
|
|
| RMS log CSV | Optuna database + export |
|
|
|
|
### Key Differences
|
|
|
|
1. **Integration**: Zernike is now an extractor like displacement/stress
|
|
2. **Optimization**: Direct use as objectives/constraints in Optuna
|
|
3. **Multi-objective**: Native NSGA-II support for RMS + mass Pareto optimization
|
|
4. **Neural Acceleration**: Can train surrogate on Zernike metrics (Protocol 12)
|
|
|
|
## Example Study Structure
|
|
|
|
```
|
|
studies/
|
|
mirror_optimization/
|
|
1_setup/
|
|
optimization_config.json
|
|
model/
|
|
ASSY_M1.prt
|
|
ASSY_M1_assyfem1.afm
|
|
ASSY_M1_assyfem1_sim1.sim
|
|
2_results/
|
|
study.db
|
|
zernike_analysis/
|
|
trial_001_zernike.json
|
|
trial_002_zernike.json
|
|
...
|
|
run_optimization.py
|
|
```
|
|
|
|
## See Also
|
|
|
|
### Related Physics Documentation
|
|
|
|
- [ZERNIKE_OPD_METHOD.md](ZERNIKE_OPD_METHOD.md) - **Rigorous OPD method for lateral displacement correction** (critical for lateral support optimization)
|
|
|
|
### Protocol Documentation
|
|
|
|
- `docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md` - Extractor specifications (E8-E10: Standard Zernike, E20-E21: OPD method)
|
|
- `docs/protocols/system/SYS_17_STUDY_INSIGHTS.md` - Insight specifications (`zernike_wfe`, `zernike_opd_comparison`)
|
|
|
|
### Skill Modules (Quick Lookup)
|
|
|
|
- `.claude/skills/modules/extractors-catalog.md` - Quick extractor reference
|
|
- `.claude/skills/modules/insights-catalog.md` - Quick insight reference
|
|
|
|
### Code Implementation
|
|
|
|
- [optimization_engine/extractors/extract_zernike.py](../../optimization_engine/extractors/extract_zernike.py) - Standard Zernike extractor
|
|
- [optimization_engine/extractors/extract_zernike_opd.py](../../optimization_engine/extractors/extract_zernike_opd.py) - **OPD-based extractor** (use for lateral supports)
|
|
- [optimization_engine/extractors/zernike_helpers.py](../../optimization_engine/extractors/zernike_helpers.py) - Helper functions and objective builders
|
|
|
|
### Example Configurations
|
|
|
|
- [examples/optimization_config_zernike_mirror.json](../examples/optimization_config_zernike_mirror.json) - Full example configuration
|