- Restructure docs/ folder (remove numeric prefixes): - 04_USER_GUIDES -> guides/ - 05_API_REFERENCE -> api/ - 06_PHYSICS -> physics/ - 07_DEVELOPMENT -> development/ - 08_ARCHIVE -> archive/ - 09_DIAGRAMS -> diagrams/ - Replace tagline 'Talk, don't click' with 'LLM-driven optimization framework' in 9 files - Create comprehensive docs/GETTING_STARTED.md: - Prerequisites and quick setup - Project structure overview - First study tutorial (Claude or manual) - Dashboard usage guide - Neural acceleration introduction - Rewrite docs/00_INDEX.md with correct paths and modern structure - Archive obsolete files: - 01_PROTOCOLS.md -> archive/historical/01_PROTOCOLS_legacy.md - 03_GETTING_STARTED.md -> archive/historical/ - ATOMIZER_PODCAST_BRIEFING.md -> archive/marketing/ - Update timestamps to 2026-01-20 across all key files - Update .gitignore to exclude docs/generated/ - Version bump: ATOMIZER_CONTEXT v1.8 -> v2.0
8.9 KiB
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
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
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:
# 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
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:
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)
{
"objectives": [
{
"name": "filtered_rms",
"direction": "minimize",
"extractor": "zernike",
"extractor_config": {
"subcase": "20",
"metric": "filtered_rms_nm"
}
}
]
}
Example: Multi-Objective (RMS + Mass)
{
"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)
{
"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:
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:
- Numeric value in SUBTITLE (preferred)
- 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
- Integration: Zernike is now an extractor like displacement/stress
- Optimization: Direct use as objectives/constraints in Optuna
- Multi-objective: Native NSGA-II support for RMS + mass Pareto optimization
- 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 - 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 - Standard Zernike extractor
- optimization_engine/extractors/extract_zernike_opd.py - OPD-based extractor (use for lateral supports)
- optimization_engine/extractors/zernike_helpers.py - Helper functions and objective builders
Example Configurations
- examples/optimization_config_zernike_mirror.json - Full example configuration