# 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_16_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