docs: add Zernike trajectory method documentation + example config
This commit is contained in:
164
docs/examples/trajectory_optimization_config.yaml
Normal file
164
docs/examples/trajectory_optimization_config.yaml
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Example: Trajectory-Based Optimization for M1 Mirror
|
||||||
|
# =====================================================
|
||||||
|
#
|
||||||
|
# This config uses the Zernike Trajectory Method for mode-specific optimization.
|
||||||
|
# Instead of weighted RMS at discrete angles, we optimize integrated metrics
|
||||||
|
# across the full 20°-60° operating range.
|
||||||
|
#
|
||||||
|
# Recommended optimizer: SAT (Surrogate-Assisted Tuning)
|
||||||
|
# - Handles expensive FEA evaluations efficiently
|
||||||
|
# - Good for 10-20 design variables
|
||||||
|
# - Builds surrogate model for intelligent sampling
|
||||||
|
|
||||||
|
study:
|
||||||
|
name: "M1_Trajectory_SAT3"
|
||||||
|
description: "Mode-specific optimization using Zernike trajectory metrics"
|
||||||
|
|
||||||
|
optimizer:
|
||||||
|
type: sat # Surrogate-Assisted Tuning
|
||||||
|
config:
|
||||||
|
n_initial: 20 # Initial random samples
|
||||||
|
n_iterations: 100 # Total iterations
|
||||||
|
surrogate: gp # Gaussian Process surrogate
|
||||||
|
acquisition: ei # Expected Improvement
|
||||||
|
|
||||||
|
# Alternative: TPE (faster, good for single objective)
|
||||||
|
# optimizer:
|
||||||
|
# type: tpe
|
||||||
|
# config:
|
||||||
|
# n_trials: 150
|
||||||
|
# n_startup_trials: 20
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DESIGN VARIABLES
|
||||||
|
# =============================================================================
|
||||||
|
# These are your wiffle tree and geometry parameters.
|
||||||
|
# Adjust ranges based on your current best design.
|
||||||
|
|
||||||
|
design_variables:
|
||||||
|
# Wiffle tree radial positions (mm)
|
||||||
|
- id: dv_wiffle_r1
|
||||||
|
name: "Wiffle R1 (inner ring)"
|
||||||
|
type: continuous
|
||||||
|
bounds: [180, 220]
|
||||||
|
|
||||||
|
- id: dv_wiffle_r2
|
||||||
|
name: "Wiffle R2 (middle ring)"
|
||||||
|
type: continuous
|
||||||
|
bounds: [320, 380]
|
||||||
|
|
||||||
|
- id: dv_wiffle_r3
|
||||||
|
name: "Wiffle R3 (outer ring)"
|
||||||
|
type: continuous
|
||||||
|
bounds: [480, 540]
|
||||||
|
|
||||||
|
# Wiffle tree angular offsets (degrees)
|
||||||
|
- id: dv_wiffle_theta
|
||||||
|
name: "Wiffle angular offset"
|
||||||
|
type: continuous
|
||||||
|
bounds: [-5, 5]
|
||||||
|
|
||||||
|
# Rib geometry
|
||||||
|
- id: dv_rib_thickness
|
||||||
|
name: "Rib thickness"
|
||||||
|
type: continuous
|
||||||
|
bounds: [8, 15]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# EXTRACTORS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
extractors:
|
||||||
|
# Primary: Trajectory-based analysis (5 angles)
|
||||||
|
- id: ext_trajectory
|
||||||
|
name: "Zernike Trajectory"
|
||||||
|
type: zernike_trajectory
|
||||||
|
config:
|
||||||
|
reference_angle: 20.0
|
||||||
|
# Angles auto-detected from OP2 subcase labels
|
||||||
|
|
||||||
|
# Fallback: Standard Zernike (for comparison)
|
||||||
|
- id: ext_zernike_40
|
||||||
|
name: "Zernike 40° vs 20°"
|
||||||
|
type: zernike_opd
|
||||||
|
config:
|
||||||
|
subcase: "40"
|
||||||
|
reference_subcase: "20"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# OBJECTIVES
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# RECOMMENDED: Start with total_filtered_rms_nm as single objective.
|
||||||
|
# Once you find a good region, switch to multi-objective with mode-specific.
|
||||||
|
|
||||||
|
objectives:
|
||||||
|
# PRIMARY: Total integrated RMS (single number for optimizer)
|
||||||
|
- id: obj_total
|
||||||
|
name: "Total Integrated RMS"
|
||||||
|
source:
|
||||||
|
extractor_id: ext_trajectory
|
||||||
|
output_name: total_filtered_rms_nm
|
||||||
|
direction: minimize
|
||||||
|
weight: 1.0
|
||||||
|
|
||||||
|
# SECONDARY: Coma (almost entirely lateral-driven)
|
||||||
|
# Useful if you want to specifically target coma reduction
|
||||||
|
- id: obj_coma
|
||||||
|
name: "Coma Integrated RMS"
|
||||||
|
source:
|
||||||
|
extractor_id: ext_trajectory
|
||||||
|
output_name: coma_rms_nm
|
||||||
|
direction: minimize
|
||||||
|
weight: 0.0 # Set to 0 for logging only, increase for multi-objective
|
||||||
|
|
||||||
|
# SECONDARY: Astigmatism
|
||||||
|
- id: obj_astig
|
||||||
|
name: "Astigmatism Integrated RMS"
|
||||||
|
source:
|
||||||
|
extractor_id: ext_trajectory
|
||||||
|
output_name: astigmatism_rms_nm
|
||||||
|
direction: minimize
|
||||||
|
weight: 0.0
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CONSTRAINTS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
# Model quality check: R² should stay high
|
||||||
|
- id: con_r2
|
||||||
|
name: "Linear fit quality"
|
||||||
|
source:
|
||||||
|
extractor_id: ext_trajectory
|
||||||
|
output_name: linear_fit_r2
|
||||||
|
type: hard
|
||||||
|
operator: ">="
|
||||||
|
threshold: 0.95
|
||||||
|
|
||||||
|
# Performance constraint from requirements
|
||||||
|
- id: con_wfe_40
|
||||||
|
name: "WFE at 40° < 14nm"
|
||||||
|
source:
|
||||||
|
extractor_id: ext_zernike_40
|
||||||
|
output_name: filtered_rms_nm
|
||||||
|
type: hard
|
||||||
|
operator: "<="
|
||||||
|
threshold: 14.0
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# EXECUTION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
execution:
|
||||||
|
# NX journal for FEA
|
||||||
|
journal: "nx_journals/run_fea_multi_subcase.py"
|
||||||
|
|
||||||
|
# Subcases to solve (must include all trajectory angles)
|
||||||
|
subcases: [90, 20, 30, 40, 50, 60]
|
||||||
|
|
||||||
|
# Timeout per iteration
|
||||||
|
timeout_seconds: 600
|
||||||
|
|
||||||
|
# Parallelism (if NX licenses available)
|
||||||
|
parallel: 1
|
||||||
269
docs/physics/ZERNIKE_TRAJECTORY_METHOD.md
Normal file
269
docs/physics/ZERNIKE_TRAJECTORY_METHOD.md
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
# Zernike Trajectory Method for Elevation-Dependent Optimization
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Created**: 2026-01-29
|
||||||
|
**Author**: Mario (Clawdbot) + Antoine Letarte
|
||||||
|
**Status**: Validated ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
The Zernike Trajectory Method provides **mode-specific optimization objectives** for telescope mirrors operating across multiple elevation angles. Instead of optimizing a weighted sum of discrete WFE values, this method:
|
||||||
|
|
||||||
|
1. **Tracks how each Zernike mode evolves** with elevation angle
|
||||||
|
2. **Fits a physics-based linear model** to the trajectory
|
||||||
|
3. **Provides integrated RMS metrics** for each aberration type
|
||||||
|
4. **Reveals which modes respond to axial vs lateral gravity loads**
|
||||||
|
|
||||||
|
**Key result**: For a well-designed support system, the linear model achieves R² ≈ 1.0, meaning deformation is entirely predictable from the gravity load decomposition.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Physics Background
|
||||||
|
|
||||||
|
### 1.1 Gravity Load Decomposition
|
||||||
|
|
||||||
|
At elevation angle θ from horizontal:
|
||||||
|
|
||||||
|
```
|
||||||
|
Axial load (along optical axis): F_axial ∝ sin(θ)
|
||||||
|
Lateral load (perpendicular): F_lateral ∝ cos(θ)
|
||||||
|
```
|
||||||
|
|
||||||
|
For a linear elastic structure, surface deformation is linear in these load components.
|
||||||
|
|
||||||
|
### 1.2 Zernike Coefficient Evolution
|
||||||
|
|
||||||
|
Each Zernike coefficient c_j follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
c_j(θ) = a_j · (sin(θ) - sin(θ_ref)) + b_j · (cos(θ) - cos(θ_ref))
|
||||||
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
- `θ_ref` = reference angle (typically 20° for polishing/measurement)
|
||||||
|
- `a_j` = sensitivity of mode j to axial load change
|
||||||
|
- `b_j` = sensitivity of mode j to lateral load change
|
||||||
|
|
||||||
|
### 1.3 The Sensitivity Matrix
|
||||||
|
|
||||||
|
Define trajectory parameters:
|
||||||
|
```
|
||||||
|
τ(θ) = [sin(θ) - sin(θ_ref), cos(θ) - cos(θ_ref)]ᵀ
|
||||||
|
```
|
||||||
|
|
||||||
|
The full coefficient vector evolves as:
|
||||||
|
```
|
||||||
|
c⃗(θ) = S · τ(θ)
|
||||||
|
```
|
||||||
|
|
||||||
|
Where **S** is the sensitivity matrix (N_modes × 2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Implementation
|
||||||
|
|
||||||
|
### 2.1 Extractor Location
|
||||||
|
|
||||||
|
```
|
||||||
|
optimization_engine/extractors/extract_zernike_trajectory.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Basic Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors.extract_zernike_trajectory import extract_zernike_trajectory
|
||||||
|
|
||||||
|
result = extract_zernike_trajectory(
|
||||||
|
'path/to/solution.op2',
|
||||||
|
reference_angle=20.0, # Reference elevation (degrees)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mode-specific integrated RMS (nm)
|
||||||
|
print(result['coma_rms_nm'])
|
||||||
|
print(result['astigmatism_rms_nm'])
|
||||||
|
print(result['trefoil_rms_nm'])
|
||||||
|
print(result['spherical_rms_nm'])
|
||||||
|
|
||||||
|
# Total filtered RMS across all angles
|
||||||
|
print(result['total_filtered_rms_nm'])
|
||||||
|
|
||||||
|
# Linear model fit quality (should be > 0.95)
|
||||||
|
print(result['linear_fit_r2'])
|
||||||
|
|
||||||
|
# Sensitivity analysis
|
||||||
|
print(result['sensitivity_matrix'])
|
||||||
|
# {'coma': {'axial': 0.63, 'lateral': 36.04, 'total': 36.05}, ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Auto-Detection Features
|
||||||
|
|
||||||
|
The extractor automatically:
|
||||||
|
- Reads subcase labels from OP2 metadata (e.g., "20", "40", "60")
|
||||||
|
- Excludes manufacturing angle (90°) by default
|
||||||
|
- Sorts angles for proper trajectory fitting
|
||||||
|
|
||||||
|
### 2.4 Output Dictionary
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
# Mode-specific integrated RMS (nm)
|
||||||
|
'coma_rms_nm': float,
|
||||||
|
'astigmatism_rms_nm': float,
|
||||||
|
'trefoil_rms_nm': float,
|
||||||
|
'spherical_rms_nm': float,
|
||||||
|
'secondary_astig_rms_nm': float,
|
||||||
|
'secondary_coma_rms_nm': float,
|
||||||
|
'quadrafoil_rms_nm': float,
|
||||||
|
|
||||||
|
# Total filtered RMS (all modes, integrated)
|
||||||
|
'total_filtered_rms_nm': float,
|
||||||
|
|
||||||
|
# Model quality
|
||||||
|
'linear_fit_r2': float, # Should be > 0.95
|
||||||
|
|
||||||
|
# Sensitivity analysis
|
||||||
|
'sensitivity_matrix': {
|
||||||
|
'coma': {'axial': float, 'lateral': float, 'total': float},
|
||||||
|
'astigmatism': {...},
|
||||||
|
...
|
||||||
|
},
|
||||||
|
|
||||||
|
# Mode ranking (by sensitivity)
|
||||||
|
'mode_ranking': ['spherical', 'coma', 'astigmatism', ...],
|
||||||
|
'dominant_mode': str,
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
'angles_deg': [20.0, 30.0, 40.0, 50.0, 60.0],
|
||||||
|
'reference_angle': 20.0,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Optimization Integration
|
||||||
|
|
||||||
|
### 3.1 AtomizerSpec Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
extractors:
|
||||||
|
- id: ext_trajectory
|
||||||
|
name: "Zernike Trajectory"
|
||||||
|
type: zernike_trajectory
|
||||||
|
config:
|
||||||
|
reference_angle: 20.0
|
||||||
|
# Subcases auto-detected from OP2 labels
|
||||||
|
|
||||||
|
objectives:
|
||||||
|
# Option A: Single combined objective
|
||||||
|
- id: obj_total
|
||||||
|
name: "Total Integrated RMS"
|
||||||
|
source:
|
||||||
|
extractor_id: ext_trajectory
|
||||||
|
output_name: total_filtered_rms_nm
|
||||||
|
direction: minimize
|
||||||
|
weight: 1.0
|
||||||
|
|
||||||
|
# Option B: Mode-specific objectives (multi-objective)
|
||||||
|
- id: obj_coma
|
||||||
|
name: "Coma RMS"
|
||||||
|
source:
|
||||||
|
extractor_id: ext_trajectory
|
||||||
|
output_name: coma_rms_nm
|
||||||
|
direction: minimize
|
||||||
|
weight: 1.0
|
||||||
|
|
||||||
|
- id: obj_astig
|
||||||
|
name: "Astigmatism RMS"
|
||||||
|
source:
|
||||||
|
extractor_id: ext_trajectory
|
||||||
|
output_name: astigmatism_rms_nm
|
||||||
|
direction: minimize
|
||||||
|
weight: 0.8
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Recommended Optimization Strategy
|
||||||
|
|
||||||
|
**For SAT (Surrogate-Assisted Tuning):**
|
||||||
|
- Use `total_filtered_rms_nm` as primary objective
|
||||||
|
- Add mode-specific objectives as secondary for Pareto analysis
|
||||||
|
- SAT handles the expensive FEA evaluations efficiently
|
||||||
|
|
||||||
|
**For TPE (Tree-Parzen Estimator):**
|
||||||
|
- Good for single-objective optimization
|
||||||
|
- Use weighted combination if needed:
|
||||||
|
```
|
||||||
|
J = w_coma * coma_rms + w_astig * astig_rms + w_total * total_rms
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Validation Results (M1 Mirror, 2026-01-29)
|
||||||
|
|
||||||
|
### 4.1 Test Configuration
|
||||||
|
- **Model**: M1 GigaBIT mirror (1.2m Zerodur)
|
||||||
|
- **Angles**: 20°, 30°, 40°, 50°, 60° (5 subcases)
|
||||||
|
- **Reference**: 20° (polishing/measurement)
|
||||||
|
|
||||||
|
### 4.2 Linear Fit Quality
|
||||||
|
```
|
||||||
|
R² = 1.0000 ← PERFECT FIT
|
||||||
|
```
|
||||||
|
|
||||||
|
The physics model is **exactly correct** for this mirror design.
|
||||||
|
|
||||||
|
### 4.3 Mode-Specific Results
|
||||||
|
|
||||||
|
| Mode | Integrated RMS (nm) | Axial Sensitivity | Lateral Sensitivity |
|
||||||
|
|------|---------------------|-------------------|---------------------|
|
||||||
|
| Secondary Astig | 12.95 | 1.93 | 48.91 |
|
||||||
|
| Spherical | 10.57 | 19.84 | 68.08 |
|
||||||
|
| Coma | 9.14 | 0.63 | **36.04** |
|
||||||
|
| Trefoil | 6.83 | — | — |
|
||||||
|
| Astigmatism | 6.80 | — | — |
|
||||||
|
|
||||||
|
**Key insight**: Coma is almost entirely driven by lateral loads (axial/lateral ratio = 0.02). Optimizing lateral support locations will have the biggest impact on coma.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Troubleshooting
|
||||||
|
|
||||||
|
### 5.1 Low R² Value
|
||||||
|
|
||||||
|
If R² < 0.9:
|
||||||
|
- Check for contact nonlinearity (supports lifting off)
|
||||||
|
- Check for material nonlinearity
|
||||||
|
- Verify subcases have consistent boundary conditions
|
||||||
|
|
||||||
|
### 5.2 Missing Subcases
|
||||||
|
|
||||||
|
Ensure your FEA includes all required angles. Minimum recommended:
|
||||||
|
- 20° (reference)
|
||||||
|
- 40° (primary operational)
|
||||||
|
- 60° (secondary operational)
|
||||||
|
|
||||||
|
Adding 30° and 50° improves trajectory fit quality.
|
||||||
|
|
||||||
|
### 5.3 Subcase Label Detection
|
||||||
|
|
||||||
|
If auto-detection fails, specify manually:
|
||||||
|
```python
|
||||||
|
result = extract_zernike_trajectory(
|
||||||
|
'solution.op2',
|
||||||
|
subcases=[2, 5, 3, 6, 4], # Subcase IDs
|
||||||
|
angles=[20.0, 30.0, 40.0, 50.0, 60.0], # Corresponding angles
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. References
|
||||||
|
|
||||||
|
- [ZERNIKE_OPD_METHOD.md](ZERNIKE_OPD_METHOD.md) — OPD correction for lateral displacement
|
||||||
|
- [ZERNIKE_FUNDAMENTALS.md](../ZERNIKE_FUNDAMENTALS.md) — Zernike polynomial basics
|
||||||
|
- AtomizerSpec v2.0 — Objective/extractor configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document maintained by Atomizer Framework. Last updated: 2026-01-29*
|
||||||
Reference in New Issue
Block a user