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

11 KiB

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).

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:

{
    '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).

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:

{
    '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).

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

# 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

# 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

# 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

{
  "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:

"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:

if is_new_study:
    sampler = TPESampler(seed=42)
else:
    sampler = TPESampler()  # No seed for resume

2. OP2 Data Validation

Always validate before processing:

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:

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