feat: create SAT3_Trajectory study with Zernike Trajectory Method
First production implementation of trajectory-based optimization for M1 mirror. Study Configuration: - Optimizer: TPE (100 trials, 15 startup) - Primary objective: total_filtered_rms_nm (integrated RMS across 20-60 deg) - Logged objectives: coma_rms_nm, astigmatism_rms_nm, trefoil_rms_nm, spherical_rms_nm - Design variables: 11 (full wiffle tree + lateral supports) - Physics validation: R² fit quality monitoring Key Features: - Mode-specific aberration tracking (coma, astigmatism, trefoil, spherical) - Physics-based trajectory model: c_j(θ) = a_j·sin(θ) + b_j·cos(θ) - Sensitivity analysis: axial vs lateral load contributions - OPD correction with focal_length=22000mm - Annular aperture (inner_radius=135.75mm) Validation Results: - Tested on existing M1_Tensor OP2: R²=1.0000 (perfect fit) - Baseline total RMS: 4.30 nm - All 5 angles auto-detected: [20, 30, 40, 50, 60] deg - Dominant mode: spherical (10.51 nm) Files Created: - studies/M1_Mirror/SAT3_Trajectory/README.md (complete documentation) - studies/M1_Mirror/SAT3_Trajectory/STUDY_REPORT.md (results template) - studies/M1_Mirror/SAT3_Trajectory/run_optimization.py (TPE + trajectory extraction) - studies/M1_Mirror/SAT3_Trajectory/1_setup/optimization_config.json (TPE config) - studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ (all NX files copied from M1_Tensor) - test_trajectory_extractor.py (validation script) References: - Physics: docs/physics/ZERNIKE_TRAJECTORY_METHOD.md - Handoff: docs/handoff/SETUP_TRAJECTORY_OPTIMIZATION.md - Extractor: optimization_engine/extractors/extract_zernike_trajectory.py Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
BIN
studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1.prt
Normal file
BIN
studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1.prt
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank.prt
Normal file
BIN
studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank.prt
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
|||||||
|
[mm]lateral_inner_u=0.32248417341983515
|
||||||
|
[mm]lateral_outer_u=0.9038210727913156
|
||||||
|
[mm]lateral_middle_pivot=21.25398896032501
|
||||||
|
[Degrees]lateral_inner_angle=30.182447933329243
|
||||||
|
[Degrees]lateral_outer_angle=15.08932828662093
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
{
|
||||||
|
"$schema": "Atomizer M1 Mirror Trajectory-Based Optimization - SAT3",
|
||||||
|
"study_name": "SAT3_Trajectory",
|
||||||
|
"study_tag": "TPE-100-TrajectoryMethod",
|
||||||
|
"description": "Trajectory-based optimization using Zernike Trajectory Method. Optimizes integrated RMS across full 20-60 deg operating range with mode-specific tracking.",
|
||||||
|
"business_context": {
|
||||||
|
"purpose": "Explore new trajectory optimization method for mode-specific aberration control",
|
||||||
|
"benefit": "Physics-based optimization with integrated metrics and sensitivity analysis",
|
||||||
|
"goal": "Minimize total_filtered_rms_nm across operating range while tracking mode-specific contributions"
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"algorithm": "TPE",
|
||||||
|
"n_trials": 100,
|
||||||
|
"n_startup_trials": 15,
|
||||||
|
"notes": "TPE recommended for fresh trajectory-based optimization - good for single-objective with logged secondaries"
|
||||||
|
},
|
||||||
|
"extraction_method": {
|
||||||
|
"type": "zernike_trajectory",
|
||||||
|
"class": "ZernikeTrajectoryExtractor",
|
||||||
|
"method": "extract_trajectory",
|
||||||
|
"reference_angle": 20.0,
|
||||||
|
"focal_length": 22000.0,
|
||||||
|
"inner_radius": 135.75,
|
||||||
|
"description": "Trajectory analysis across 5 elevation angles (20, 30, 40, 50, 60 deg) with OPD correction and annular aperture"
|
||||||
|
},
|
||||||
|
"design_variables": [
|
||||||
|
{
|
||||||
|
"name": "lateral_inner_angle",
|
||||||
|
"expression_name": "lateral_inner_angle",
|
||||||
|
"min": 25.0,
|
||||||
|
"max": 30.0,
|
||||||
|
"baseline": 26.79,
|
||||||
|
"units": "degrees",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Inner lateral support angle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lateral_outer_angle",
|
||||||
|
"expression_name": "lateral_outer_angle",
|
||||||
|
"min": 11.0,
|
||||||
|
"max": 17.0,
|
||||||
|
"baseline": 14.64,
|
||||||
|
"units": "degrees",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Outer lateral support angle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lateral_outer_pivot",
|
||||||
|
"expression_name": "lateral_outer_pivot",
|
||||||
|
"min": 9.0,
|
||||||
|
"max": 12.0,
|
||||||
|
"baseline": 10.40,
|
||||||
|
"units": "mm",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Outer lateral pivot position"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lateral_inner_pivot",
|
||||||
|
"expression_name": "lateral_inner_pivot",
|
||||||
|
"min": 5.0,
|
||||||
|
"max": 12.0,
|
||||||
|
"baseline": 10.07,
|
||||||
|
"units": "mm",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Inner lateral pivot position"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lateral_middle_pivot",
|
||||||
|
"expression_name": "lateral_middle_pivot",
|
||||||
|
"min": 15.0,
|
||||||
|
"max": 27.0,
|
||||||
|
"baseline": 20.73,
|
||||||
|
"units": "mm",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Middle lateral pivot position"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lateral_closeness",
|
||||||
|
"expression_name": "lateral_closeness",
|
||||||
|
"min": 9.5,
|
||||||
|
"max": 12.5,
|
||||||
|
"baseline": 11.02,
|
||||||
|
"units": "mm",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Lateral support closeness parameter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "whiffle_min",
|
||||||
|
"expression_name": "whiffle_min",
|
||||||
|
"min": 30.0,
|
||||||
|
"max": 72.0,
|
||||||
|
"baseline": 40.55,
|
||||||
|
"units": "mm",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Whiffle tree minimum radius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "whiffle_outer_to_vertical",
|
||||||
|
"expression_name": "whiffle_outer_to_vertical",
|
||||||
|
"min": 60.0,
|
||||||
|
"max": 80.0,
|
||||||
|
"baseline": 75.67,
|
||||||
|
"units": "degrees",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Whiffle tree outer angle to vertical"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "whiffle_triangle_closeness",
|
||||||
|
"expression_name": "whiffle_triangle_closeness",
|
||||||
|
"min": 50.0,
|
||||||
|
"max": 80.0,
|
||||||
|
"baseline": 60.00,
|
||||||
|
"units": "mm",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Whiffle tree triangle closeness"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "blank_backface_angle",
|
||||||
|
"expression_name": "blank_backface_angle",
|
||||||
|
"min": 4.1,
|
||||||
|
"max": 4.5,
|
||||||
|
"baseline": 4.15,
|
||||||
|
"units": "degrees",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Blank backface angle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inner_circular_rib_dia",
|
||||||
|
"expression_name": "inner_circular_rib_dia",
|
||||||
|
"min": 480.0,
|
||||||
|
"max": 620.0,
|
||||||
|
"baseline": 534.00,
|
||||||
|
"units": "mm",
|
||||||
|
"enabled": true,
|
||||||
|
"notes": "Inner circular rib diameter"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fixed_parameters": [],
|
||||||
|
"constraints": [
|
||||||
|
{
|
||||||
|
"name": "trajectory_fit_quality",
|
||||||
|
"type": "soft",
|
||||||
|
"expression": "linear_fit_r2 >= 0.95",
|
||||||
|
"description": "Linear trajectory model should fit well (R² ≥ 0.95)",
|
||||||
|
"penalty_weight": 100.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "blank_mass_max",
|
||||||
|
"type": "hard",
|
||||||
|
"expression": "mass_kg <= 120.0",
|
||||||
|
"description": "Maximum blank mass constraint",
|
||||||
|
"penalty_weight": 1000.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"objectives": [
|
||||||
|
{
|
||||||
|
"name": "total_filtered_rms_nm",
|
||||||
|
"description": "Total integrated RMS across full operating range (20-60 deg)",
|
||||||
|
"direction": "minimize",
|
||||||
|
"weight": 1.0,
|
||||||
|
"target": 4.0,
|
||||||
|
"units": "nm",
|
||||||
|
"notes": "PRIMARY OBJECTIVE - optimized by TPE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "coma_rms_nm",
|
||||||
|
"description": "Integrated coma RMS (modes Z7,Z8)",
|
||||||
|
"direction": "minimize",
|
||||||
|
"weight": 0.0,
|
||||||
|
"target": 5.0,
|
||||||
|
"units": "nm",
|
||||||
|
"notes": "LOGGED ONLY - tracks coma contribution"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "astigmatism_rms_nm",
|
||||||
|
"description": "Integrated astigmatism RMS (modes Z5,Z6)",
|
||||||
|
"direction": "minimize",
|
||||||
|
"weight": 0.0,
|
||||||
|
"target": 5.0,
|
||||||
|
"units": "nm",
|
||||||
|
"notes": "LOGGED ONLY - tracks astigmatism contribution"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trefoil_rms_nm",
|
||||||
|
"description": "Integrated trefoil RMS (modes Z9,Z10)",
|
||||||
|
"direction": "minimize",
|
||||||
|
"weight": 0.0,
|
||||||
|
"target": 5.0,
|
||||||
|
"units": "nm",
|
||||||
|
"notes": "LOGGED ONLY - tracks trefoil contribution"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spherical_rms_nm",
|
||||||
|
"description": "Integrated spherical RMS (mode Z11)",
|
||||||
|
"direction": "minimize",
|
||||||
|
"weight": 0.0,
|
||||||
|
"target": 8.0,
|
||||||
|
"units": "nm",
|
||||||
|
"notes": "LOGGED ONLY - tracks spherical aberration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linear_fit_r2",
|
||||||
|
"description": "Trajectory model fit quality (should be ~1.0)",
|
||||||
|
"direction": "maximize",
|
||||||
|
"weight": 0.0,
|
||||||
|
"target": 0.95,
|
||||||
|
"units": "unitless",
|
||||||
|
"notes": "LOGGED ONLY - validates physics model"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weighted_sum_formula": "total_filtered_rms_nm (primary) + 0*coma_rms_nm + 0*astigmatism_rms_nm + 0*trefoil_rms_nm + 0*spherical_rms_nm",
|
||||||
|
"zernike_settings": {
|
||||||
|
"n_modes": 50,
|
||||||
|
"filter_low_orders": 4,
|
||||||
|
"displacement_unit": "mm",
|
||||||
|
"subcases": ["1", "2", "5", "3", "6", "4"],
|
||||||
|
"subcase_labels": {
|
||||||
|
"1": "90deg",
|
||||||
|
"2": "20deg",
|
||||||
|
"3": "40deg",
|
||||||
|
"4": "60deg",
|
||||||
|
"5": "30deg",
|
||||||
|
"6": "50deg"
|
||||||
|
},
|
||||||
|
"reference_subcase": "2",
|
||||||
|
"method": "trajectory",
|
||||||
|
"reference_angle": 20.0,
|
||||||
|
"focal_length": 22000.0,
|
||||||
|
"inner_radius": 135.75,
|
||||||
|
"exclude_angles": [90.0]
|
||||||
|
},
|
||||||
|
"nx_settings": {
|
||||||
|
"nx_install_path": "C:\\Program Files\\Siemens\\DesigncenterNX2512",
|
||||||
|
"sim_file": "ASSY_M1_assyfem1_sim1.sim",
|
||||||
|
"solution_name": "Solution 1",
|
||||||
|
"op2_pattern": "*-solution_1.op2",
|
||||||
|
"simulation_timeout_s": 600,
|
||||||
|
"journal_timeout_s": 120,
|
||||||
|
"op2_timeout_s": 1800,
|
||||||
|
"auto_start_nx": true
|
||||||
|
},
|
||||||
|
"dashboard_settings": {
|
||||||
|
"trial_source_tag": true,
|
||||||
|
"fea_marker": "circle",
|
||||||
|
"fea_color": "#4CAF50"
|
||||||
|
}
|
||||||
|
}
|
||||||
221
studies/M1_Mirror/SAT3_Trajectory/README.md
Normal file
221
studies/M1_Mirror/SAT3_Trajectory/README.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# SAT3_Trajectory - Zernike Trajectory Method Optimization
|
||||||
|
|
||||||
|
**Status:** Active
|
||||||
|
**Created:** 2026-01-29
|
||||||
|
**Method:** Zernike Trajectory Analysis
|
||||||
|
**Optimizer:** TPE (Tree-Parzen Estimator)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
First production implementation of the **Zernike Trajectory Method** for M1 mirror optimization. Instead of optimizing discrete WFE values at fixed angles, this study optimizes integrated RMS metrics across the full 20°-60° operating range with mode-specific aberration tracking.
|
||||||
|
|
||||||
|
**Key Innovation:** Physics-based trajectory model tracks how each Zernike mode (coma, astigmatism, trefoil, spherical) evolves with elevation angle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Study Overview
|
||||||
|
|
||||||
|
### Primary Objective
|
||||||
|
- **total_filtered_rms_nm** - Integrated RMS across full operating range (weight=1.0)
|
||||||
|
|
||||||
|
### Logged Objectives (Not Optimized, weight=0)
|
||||||
|
- **coma_rms_nm** - Coma aberration (Z7, Z8)
|
||||||
|
- **astigmatism_rms_nm** - Astigmatism (Z5, Z6)
|
||||||
|
- **trefoil_rms_nm** - Trefoil (Z9, Z10)
|
||||||
|
- **spherical_rms_nm** - Spherical aberration (Z11)
|
||||||
|
- **linear_fit_r2** - Physics model validation (should be ~1.0)
|
||||||
|
|
||||||
|
### Design Variables (11 total)
|
||||||
|
|
||||||
|
| Parameter | Min | Max | Baseline | Units | Category |
|
||||||
|
|-----------|-----|-----|----------|-------|----------|
|
||||||
|
| lateral_inner_angle | 25.0 | 30.0 | 26.79 | deg | Lateral Support |
|
||||||
|
| lateral_outer_angle | 11.0 | 17.0 | 14.64 | deg | Lateral Support |
|
||||||
|
| lateral_outer_pivot | 9.0 | 12.0 | 10.40 | mm | Lateral Support |
|
||||||
|
| lateral_inner_pivot | 5.0 | 12.0 | 10.07 | mm | Lateral Support |
|
||||||
|
| lateral_middle_pivot | 15.0 | 27.0 | 20.73 | mm | Lateral Support |
|
||||||
|
| lateral_closeness | 9.5 | 12.5 | 11.02 | mm | Lateral Support |
|
||||||
|
| whiffle_min | 30.0 | 72.0 | 40.55 | mm | Whiffle Tree |
|
||||||
|
| whiffle_outer_to_vertical | 60.0 | 80.0 | 75.67 | deg | Whiffle Tree |
|
||||||
|
| whiffle_triangle_closeness | 50.0 | 80.0 | 60.00 | mm | Whiffle Tree |
|
||||||
|
| blank_backface_angle | 4.1 | 4.5 | 4.15 | deg | Geometry |
|
||||||
|
| inner_circular_rib_dia | 480.0 | 620.0 | 534.00 | mm | Geometry |
|
||||||
|
|
||||||
|
### Optimizer Configuration
|
||||||
|
- **Algorithm:** TPE (Tree-Parzen Estimator)
|
||||||
|
- **Budget:** 100 trials
|
||||||
|
- **Startup Trials:** 15 (random sampling for initial exploration)
|
||||||
|
- **Seed:** 42 (for reproducibility)
|
||||||
|
|
||||||
|
### Constraints
|
||||||
|
1. **Mass:** blank_mass <= 120 kg (hard constraint, penalty=1e6)
|
||||||
|
2. **R² fit:** linear_fit_r2 >= 0.95 (soft constraint, ensures physics model validity)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trajectory Method Details
|
||||||
|
|
||||||
|
### Physics Basis
|
||||||
|
|
||||||
|
At elevation angle θ, gravity decomposes into:
|
||||||
|
```
|
||||||
|
Axial load: F_axial ∝ sin(θ)
|
||||||
|
Lateral load: F_lateral ∝ cos(θ)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each Zernike coefficient follows:
|
||||||
|
```
|
||||||
|
c_j(θ) = a_j·(sin θ - sin θ_ref) + b_j·(cos θ - cos θ_ref)
|
||||||
|
```
|
||||||
|
|
||||||
|
The sensitivity matrix `[a_j, b_j]` reveals which modes respond to axial vs lateral loads.
|
||||||
|
|
||||||
|
### Elevation Angles Analyzed
|
||||||
|
- **90°** - Manufacturing reference (excluded from trajectory)
|
||||||
|
- **20°** - Measurement/polishing reference
|
||||||
|
- **30°** - New trajectory point
|
||||||
|
- **40°** - Primary operational angle
|
||||||
|
- **50°** - New trajectory point
|
||||||
|
- **60°** - Secondary operational angle
|
||||||
|
|
||||||
|
### Extractor Configuration
|
||||||
|
- **Method:** Zernike Trajectory
|
||||||
|
- **Reference Angle:** 20° (polishing/measurement)
|
||||||
|
- **Focal Length:** 22000 mm (OPD correction for lateral displacements)
|
||||||
|
- **Inner Radius:** 135.75 mm (annular aperture, excludes 271.5mm central hole)
|
||||||
|
- **N Modes:** 50 (filtered from mode 5 onward)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expected Performance
|
||||||
|
|
||||||
|
### Baseline (from test on M1_Tensor model)
|
||||||
|
- **Total Filtered RMS:** 4.30 nm
|
||||||
|
- **Coma RMS:** 9.16 nm
|
||||||
|
- **Astigmatism RMS:** 6.55 nm
|
||||||
|
- **Trefoil RMS:** 6.44 nm
|
||||||
|
- **Spherical RMS:** 10.51 nm
|
||||||
|
- **R² fit:** 1.0000 (perfect)
|
||||||
|
- **Dominant mode:** Spherical
|
||||||
|
|
||||||
|
### Optimization Targets
|
||||||
|
- **Total Filtered RMS:** < 4.0 nm
|
||||||
|
- **Coma RMS:** < 5.0 nm
|
||||||
|
- **R² fit:** > 0.95 (validates physics model)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Start Optimization
|
||||||
|
```bash
|
||||||
|
cd studies/M1_Mirror/SAT3_Trajectory
|
||||||
|
python run_optimization.py --start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resume Optimization
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --start --resume
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Trial Count
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --start --trials 150
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Single FEA Run
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analyze Results
|
||||||
|
```bash
|
||||||
|
# View convergence
|
||||||
|
python -m optimization_engine.reporting.visualizer 3_results/study.db
|
||||||
|
|
||||||
|
# Generate report
|
||||||
|
python -m optimization_engine.reporting.markdown_report 3_results/study.db
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
SAT3_Trajectory/
|
||||||
|
├── README.md (This file)
|
||||||
|
├── STUDY_REPORT.md (Results report - updated after optimization)
|
||||||
|
├── run_optimization.py (Main optimization script)
|
||||||
|
├── 1_setup/
|
||||||
|
│ ├── optimization_config.json (Study configuration)
|
||||||
|
│ └── model/ (NX model files - copied from M1_Tensor)
|
||||||
|
│ ├── ASSY_M1.prt
|
||||||
|
│ ├── ASSY_M1_assyfem1.afm
|
||||||
|
│ ├── ASSY_M1_assyfem1_sim1.sim
|
||||||
|
│ ├── M1_Blank.prt
|
||||||
|
│ ├── M1_Blank_fem1.fem
|
||||||
|
│ ├── M1_Blank_fem1_i.prt
|
||||||
|
│ ├── M1_Vertical_Support_Skeleton.prt
|
||||||
|
│ ├── M1_Vertical_Support_Skeleton_fem1.fem
|
||||||
|
│ └── M1_Vertical_Support_Skeleton_fem1_i.prt
|
||||||
|
├── 2_iterations/
|
||||||
|
│ ├── iter_0001/ (Trial 1 FEA files)
|
||||||
|
│ ├── iter_0002/ (Trial 2 FEA files)
|
||||||
|
│ └── ...
|
||||||
|
└── 3_results/
|
||||||
|
├── study.db (Optuna database)
|
||||||
|
├── optimization.log (Execution log)
|
||||||
|
└── trajectory_analysis/ (Mode-specific analysis plots)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Differences from Previous Studies
|
||||||
|
|
||||||
|
### vs. Discrete WFE Optimization (V11-V15)
|
||||||
|
- **Old:** Optimize `6*wfe_40_20 + 5*wfe_60_20 + 3*mfg_90`
|
||||||
|
- **New:** Optimize integrated RMS across continuous operating range
|
||||||
|
- **Benefit:** Physics-based, mode-specific tracking, better understanding of support behavior
|
||||||
|
|
||||||
|
### vs. SAT (Surrogate-Assisted Tuning)
|
||||||
|
- **SAT:** Builds neural surrogate for fast exploration (100 FEA + 10K surrogate)
|
||||||
|
- **TPE:** Direct Bayesian optimization (100 FEA, no surrogate)
|
||||||
|
- **This Study:** TPE for initial trajectory exploration, may switch to SAT later
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **NX 2512** (FEA solver)
|
||||||
|
- **Python 3.9+** (Atomizer environment)
|
||||||
|
- **Optuna** (TPE sampler)
|
||||||
|
- **pyNastran** (OP2 reading)
|
||||||
|
- **NumPy** (trajectory fitting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **Physics Documentation:** `docs/physics/ZERNIKE_TRAJECTORY_METHOD.md`
|
||||||
|
- **Implementation:** `optimization_engine/extractors/extract_zernike_trajectory.py`
|
||||||
|
- **Example Config:** `docs/examples/trajectory_optimization_config.yaml`
|
||||||
|
- **Handoff Doc:** `docs/handoff/SETUP_TRAJECTORY_OPTIMIZATION.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
1. **R² Monitoring:** If R² drops below 0.95, it indicates nonlinearity (e.g., contact lifting). Designs with poor R² should be investigated.
|
||||||
|
|
||||||
|
2. **Mode-Specific Insights:** After optimization, analyze which modes improved most. Coma improvements indicate lateral support changes were effective.
|
||||||
|
|
||||||
|
3. **Comparison with V15:** After completion, compare trajectory-based results with V15 NSGA-II Pareto front to validate new method.
|
||||||
|
|
||||||
|
4. **Future Work:** If this study succeeds, extend to SAT with trajectory objectives for 10K+ design exploration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Study created by Atomizer, 2026-01-29*
|
||||||
|
*First implementation of Zernike Trajectory Method for M1 GigaBIT mirror*
|
||||||
168
studies/M1_Mirror/SAT3_Trajectory/STUDY_REPORT.md
Normal file
168
studies/M1_Mirror/SAT3_Trajectory/STUDY_REPORT.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# SAT3_Trajectory - Study Report
|
||||||
|
|
||||||
|
**Status:** _pending optimization_
|
||||||
|
**Optimization Started:** _pending_
|
||||||
|
**Optimization Completed:** _pending_
|
||||||
|
**Total Trials:** _pending_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optimization Summary
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Algorithm | TPE (Tree-Parzen Estimator) |
|
||||||
|
| Design Variables | 11 |
|
||||||
|
| Total Trials | _pending_ |
|
||||||
|
| Successful FEA | _pending_ |
|
||||||
|
| Failed FEA | _pending_ |
|
||||||
|
| Best Trial Number | _pending_ |
|
||||||
|
| Best Weighted Sum | _pending_ nm |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Design
|
||||||
|
|
||||||
|
### Objectives
|
||||||
|
|
||||||
|
| Objective | Best Value | Baseline | Improvement |
|
||||||
|
|-----------|------------|----------|-------------|
|
||||||
|
| **total_filtered_rms_nm** (PRIMARY) | _pending_ nm | 4.30 nm | _pending_ % |
|
||||||
|
| coma_rms_nm (logged) | _pending_ nm | 9.16 nm | _pending_ % |
|
||||||
|
| astigmatism_rms_nm (logged) | _pending_ nm | 6.55 nm | _pending_ % |
|
||||||
|
| trefoil_rms_nm (logged) | _pending_ nm | 6.44 nm | _pending_ % |
|
||||||
|
| spherical_rms_nm (logged) | _pending_ nm | 10.51 nm | _pending_ % |
|
||||||
|
| linear_fit_r2 | _pending_ | 1.0000 | _pending_ |
|
||||||
|
| mass_kg | _pending_ kg | _pending_ kg | _pending_ % |
|
||||||
|
|
||||||
|
### Design Parameters
|
||||||
|
|
||||||
|
| Parameter | Best Value | Baseline | Delta |
|
||||||
|
|-----------|------------|----------|-------|
|
||||||
|
| lateral_inner_angle | _pending_ deg | 26.79 deg | _pending_ |
|
||||||
|
| lateral_outer_angle | _pending_ deg | 14.64 deg | _pending_ |
|
||||||
|
| lateral_outer_pivot | _pending_ mm | 10.40 mm | _pending_ |
|
||||||
|
| lateral_inner_pivot | _pending_ mm | 10.07 mm | _pending_ |
|
||||||
|
| lateral_middle_pivot | _pending_ mm | 20.73 mm | _pending_ |
|
||||||
|
| lateral_closeness | _pending_ mm | 11.02 mm | _pending_ |
|
||||||
|
| whiffle_min | _pending_ mm | 40.55 mm | _pending_ |
|
||||||
|
| whiffle_outer_to_vertical | _pending_ deg | 75.67 deg | _pending_ |
|
||||||
|
| whiffle_triangle_closeness | _pending_ mm | 60.00 mm | _pending_ |
|
||||||
|
| blank_backface_angle | _pending_ deg | 4.15 deg | _pending_ |
|
||||||
|
| inner_circular_rib_dia | _pending_ mm | 534.00 mm | _pending_ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mode-Specific Analysis
|
||||||
|
|
||||||
|
### Which Modes Improved Most?
|
||||||
|
|
||||||
|
_Analysis pending - shows which aberration types benefited from optimization_
|
||||||
|
|
||||||
|
### Sensitivity Matrix
|
||||||
|
|
||||||
|
_Analysis pending - shows which modes respond to axial vs lateral loads_
|
||||||
|
|
||||||
|
### R² Validation
|
||||||
|
|
||||||
|
_Analysis pending - confirms physics model held throughout optimization_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Convergence Analysis
|
||||||
|
|
||||||
|
### Total Filtered RMS vs Trial
|
||||||
|
|
||||||
|
_Plot pending_
|
||||||
|
|
||||||
|
### Mode-Specific RMS vs Trial
|
||||||
|
|
||||||
|
_Plot pending - overlay coma, astigmatism, trefoil, spherical_
|
||||||
|
|
||||||
|
### Parameter Evolution
|
||||||
|
|
||||||
|
_Plot pending - shows how design variables evolved_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Findings
|
||||||
|
|
||||||
|
### 1. Trajectory Method Validation
|
||||||
|
|
||||||
|
_Analysis pending_
|
||||||
|
|
||||||
|
- Did R² stay > 0.95 throughout?
|
||||||
|
- Were any designs nonlinear?
|
||||||
|
- Did the physics model hold?
|
||||||
|
|
||||||
|
### 2. Mode-Specific Insights
|
||||||
|
|
||||||
|
_Analysis pending_
|
||||||
|
|
||||||
|
- Which modes improved most?
|
||||||
|
- Which modes are dominant now?
|
||||||
|
- Does coma reduction correlate with lateral support changes?
|
||||||
|
|
||||||
|
### 3. Comparison with V15 NSGA-II
|
||||||
|
|
||||||
|
_Analysis pending_
|
||||||
|
|
||||||
|
- How does best trajectory result compare to V15 Pareto front?
|
||||||
|
- Is trajectory method competitive?
|
||||||
|
- What insights does trajectory provide that discrete WFE doesn't?
|
||||||
|
|
||||||
|
### 4. Lateral vs Axial Sensitivity
|
||||||
|
|
||||||
|
_Analysis pending_
|
||||||
|
|
||||||
|
- Which parameters affect which modes?
|
||||||
|
- Are lateral supports primarily controlling coma (as predicted)?
|
||||||
|
- Are axial supports (whiffle tree) controlling spherical?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
_Analysis pending - based on results, suggest:_
|
||||||
|
|
||||||
|
1. Whether to proceed with SAT trajectory optimization
|
||||||
|
2. Which modes need further attention
|
||||||
|
3. Whether to refine parameter bounds
|
||||||
|
4. Additional angles to include in trajectory
|
||||||
|
|
||||||
|
### Parameter Bounds Refinement
|
||||||
|
|
||||||
|
_Analysis pending_
|
||||||
|
|
||||||
|
- Did any parameters hit bounds?
|
||||||
|
- Should ranges be expanded or narrowed?
|
||||||
|
|
||||||
|
### Future Studies
|
||||||
|
|
||||||
|
_Analysis pending_
|
||||||
|
|
||||||
|
- SAT3_Trajectory_SAT (100 FEA + 10K surrogate)
|
||||||
|
- Multi-objective trajectory (optimize modes separately)
|
||||||
|
- Trajectory + mass trade-off
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Generated
|
||||||
|
|
||||||
|
- `3_results/study.db` - Optuna database with all trials
|
||||||
|
- `3_results/optimization.log` - Full execution log
|
||||||
|
- `2_iterations/iter_XXXX/` - FEA results for each trial
|
||||||
|
- _(Trajectory analysis plots - TBD)_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
_To be filled after optimization completes_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Report template created: 2026-01-29*
|
||||||
|
*To be updated after optimization completes*
|
||||||
485
studies/M1_Mirror/SAT3_Trajectory/run_optimization.py
Normal file
485
studies/M1_Mirror/SAT3_Trajectory/run_optimization.py
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
M1 Mirror SAT3_Trajectory - Trajectory-Based Optimization (TPE)
|
||||||
|
================================================================
|
||||||
|
|
||||||
|
First implementation of Zernike Trajectory Method for M1 mirror optimization.
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
1. TPE sampler - 100 trials, 15 startup
|
||||||
|
2. Trajectory analysis across 5 angles (20°, 30°, 40°, 50°, 60°)
|
||||||
|
3. Primary objective: total_filtered_rms_nm (integrated RMS across operating range)
|
||||||
|
4. Logged objectives (weight=0): coma_rms_nm, astigmatism_rms_nm, trefoil_rms_nm, spherical_rms_nm
|
||||||
|
5. Full wiffle tree + lateral support parameters (11 design variables)
|
||||||
|
6. OPD correction with focal_length=22000mm and annular aperture (inner_radius=135.75mm)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python run_optimization.py --start
|
||||||
|
python run_optimization.py --start --trials 100
|
||||||
|
python run_optimization.py --start --trials 100 --resume
|
||||||
|
python run_optimization.py --test # Single trial test
|
||||||
|
|
||||||
|
Author: Atomizer
|
||||||
|
Created: 2026-01-29
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
LICENSE_SERVER = "28000@dalidou;28000@100.80.199.40"
|
||||||
|
os.environ['SPLM_LICENSE_SERVER'] = LICENSE_SERVER
|
||||||
|
print(f"[LICENSE] SPLM_LICENSE_SERVER set to: {LICENSE_SERVER}")
|
||||||
|
|
||||||
|
# Add Atomizer root to path
|
||||||
|
STUDY_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(STUDY_DIR)))
|
||||||
|
sys.path.insert(0, PROJECT_ROOT)
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional, Any
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Dashboard Auto-Launch
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def launch_dashboard():
|
||||||
|
"""Launch the Atomizer dashboard in background."""
|
||||||
|
dashboard_dir = Path(PROJECT_ROOT) / "atomizer-dashboard"
|
||||||
|
start_script = dashboard_dir / "start-dashboard.bat"
|
||||||
|
|
||||||
|
if not start_script.exists():
|
||||||
|
print(f"[DASHBOARD] Warning: start-dashboard.bat not found at {start_script}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.Popen(
|
||||||
|
["cmd", "/c", "start", "/min", str(start_script)],
|
||||||
|
cwd=str(dashboard_dir),
|
||||||
|
shell=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
print("[DASHBOARD] Launched in background")
|
||||||
|
print("[DASHBOARD] Frontend: http://localhost:5173")
|
||||||
|
print("[DASHBOARD] Backend: http://localhost:8000")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DASHBOARD] Failed to launch: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
import optuna
|
||||||
|
from optuna.samplers import TPESampler
|
||||||
|
|
||||||
|
# Atomizer imports
|
||||||
|
from optimization_engine.nx.solver import NXSolver
|
||||||
|
from optimization_engine.extractors.extract_zernike_trajectory import extract_zernike_trajectory
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Paths
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
STUDY_DIR = Path(__file__).parent
|
||||||
|
SETUP_DIR = STUDY_DIR / "1_setup"
|
||||||
|
MODEL_DIR = SETUP_DIR / "model"
|
||||||
|
ITERATIONS_DIR = STUDY_DIR / "2_iterations"
|
||||||
|
RESULTS_DIR = STUDY_DIR / "3_results"
|
||||||
|
CONFIG_PATH = SETUP_DIR / "optimization_config.json"
|
||||||
|
|
||||||
|
# Ensure directories exist
|
||||||
|
ITERATIONS_DIR.mkdir(exist_ok=True)
|
||||||
|
RESULTS_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_FILE = RESULTS_DIR / "optimization.log"
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s | %(levelname)-8s | %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
logging.FileHandler(LOG_FILE, mode='a')
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
with open(CONFIG_PATH) as f:
|
||||||
|
CONFIG = json.load(f)
|
||||||
|
|
||||||
|
STUDY_NAME = CONFIG["study_name"]
|
||||||
|
|
||||||
|
# Primary objective is total_filtered_rms_nm (weight=1.0)
|
||||||
|
# All other objectives are logged only (weight=0)
|
||||||
|
def compute_weighted_sum(objectives: Dict[str, float]) -> float:
|
||||||
|
"""Compute weighted sum - only total_filtered_rms_nm has weight=1.0."""
|
||||||
|
return objectives.get('total_filtered_rms_nm', 1000.0)
|
||||||
|
|
||||||
|
|
||||||
|
# Hard constraint: blank_mass <= 120kg
|
||||||
|
MAX_BLANK_MASS_KG = 120.0
|
||||||
|
CONSTRAINT_PENALTY = 1e6
|
||||||
|
|
||||||
|
# Trajectory settings
|
||||||
|
REFERENCE_ANGLE = CONFIG['extraction_method'].get('reference_angle', 20.0)
|
||||||
|
FOCAL_LENGTH = CONFIG['extraction_method'].get('focal_length', 22000.0)
|
||||||
|
INNER_RADIUS_MM = CONFIG['extraction_method'].get('inner_radius', 135.75)
|
||||||
|
|
||||||
|
|
||||||
|
def check_mass_constraint(mass_kg: float) -> tuple:
|
||||||
|
"""Check if mass constraint is satisfied."""
|
||||||
|
if mass_kg <= MAX_BLANK_MASS_KG:
|
||||||
|
return True, 0.0
|
||||||
|
else:
|
||||||
|
return False, mass_kg - MAX_BLANK_MASS_KG
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# FEA Runner with Trajectory Extraction
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class FEARunner:
|
||||||
|
"""Runs FEA simulations with Zernike Trajectory extraction."""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any]):
|
||||||
|
self.config = config
|
||||||
|
self.nx_solver = None
|
||||||
|
self.master_model_dir = MODEL_DIR
|
||||||
|
|
||||||
|
# Get fixed parameter values
|
||||||
|
self.fixed_params = {}
|
||||||
|
for fp in config.get('fixed_parameters', []):
|
||||||
|
self.fixed_params[fp['name']] = fp['value']
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
"""Setup NX solver (assumes NX is already running)."""
|
||||||
|
study_name = self.config.get('study_name', 'SAT3_Trajectory')
|
||||||
|
|
||||||
|
nx_settings = self.config.get('nx_settings', {})
|
||||||
|
nx_install_dir = nx_settings.get('nx_install_path', 'C:\\Program Files\\Siemens\\DesigncenterNX2512')
|
||||||
|
version_match = re.search(r'NX(\d+)|DesigncenterNX(\d+)', nx_install_dir)
|
||||||
|
nastran_version = (version_match.group(1) or version_match.group(2)) if version_match else "2512"
|
||||||
|
|
||||||
|
self.nx_solver = NXSolver(
|
||||||
|
master_model_dir=str(self.master_model_dir),
|
||||||
|
nx_install_dir=nx_install_dir,
|
||||||
|
nastran_version=nastran_version,
|
||||||
|
timeout=nx_settings.get('simulation_timeout_s', 600),
|
||||||
|
use_iteration_folders=True,
|
||||||
|
study_name=study_name
|
||||||
|
)
|
||||||
|
logger.info(f"[NX] Solver ready (Nastran {nastran_version})")
|
||||||
|
|
||||||
|
def run_fea(self, params: Dict[str, float], trial_num: int) -> Optional[Dict]:
|
||||||
|
"""Run FEA and extract objectives using Zernike Trajectory Method."""
|
||||||
|
if self.nx_solver is None:
|
||||||
|
self.setup()
|
||||||
|
|
||||||
|
logger.info(f" [FEA {trial_num}] Running simulation...")
|
||||||
|
|
||||||
|
# Build expressions
|
||||||
|
expressions = {}
|
||||||
|
for name, value in self.fixed_params.items():
|
||||||
|
expressions[name] = value
|
||||||
|
for var in self.config['design_variables']:
|
||||||
|
if var.get('enabled', True) and var['name'] in params:
|
||||||
|
expressions[var['expression_name']] = params[var['name']]
|
||||||
|
|
||||||
|
iter_folder = self.nx_solver.create_iteration_folder(
|
||||||
|
iterations_base_dir=ITERATIONS_DIR,
|
||||||
|
iteration_number=trial_num,
|
||||||
|
expression_updates=expressions
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
nx_settings = self.config.get('nx_settings', {})
|
||||||
|
sim_file = iter_folder / nx_settings.get('sim_file', 'ASSY_M1_assyfem1_sim1.sim')
|
||||||
|
|
||||||
|
t_start = time.time()
|
||||||
|
|
||||||
|
result = self.nx_solver.run_simulation(
|
||||||
|
sim_file=sim_file,
|
||||||
|
working_dir=iter_folder,
|
||||||
|
expression_updates=expressions,
|
||||||
|
solution_name=nx_settings.get('solution_name', 'Solution 1'),
|
||||||
|
cleanup=False
|
||||||
|
)
|
||||||
|
|
||||||
|
solve_time = time.time() - t_start
|
||||||
|
|
||||||
|
if not result['success']:
|
||||||
|
logger.error(f" [FEA {trial_num}] Solve failed: {result.get('error')}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(f" [FEA {trial_num}] Solved in {solve_time:.1f}s")
|
||||||
|
|
||||||
|
# Extract objectives using Zernike Trajectory Method
|
||||||
|
op2_path = Path(result['op2_file'])
|
||||||
|
objectives = self._extract_objectives_trajectory(op2_path, iter_folder)
|
||||||
|
|
||||||
|
if objectives is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check constraint
|
||||||
|
mass_kg = objectives['mass_kg']
|
||||||
|
is_feasible, violation = check_mass_constraint(mass_kg)
|
||||||
|
|
||||||
|
if is_feasible:
|
||||||
|
weighted_sum = compute_weighted_sum(objectives)
|
||||||
|
constraint_status = "OK"
|
||||||
|
else:
|
||||||
|
weighted_sum = compute_weighted_sum(objectives) + CONSTRAINT_PENALTY * violation
|
||||||
|
constraint_status = f"VIOLATED (+{violation:.1f}kg)"
|
||||||
|
|
||||||
|
logger.info(f" [FEA {trial_num}] Total RMS: {objectives['total_filtered_rms_nm']:.2f} nm (PRIMARY)")
|
||||||
|
logger.info(f" [FEA {trial_num}] Coma RMS: {objectives['coma_rms_nm']:.2f} nm (logged)")
|
||||||
|
logger.info(f" [FEA {trial_num}] Astig RMS: {objectives['astigmatism_rms_nm']:.2f} nm (logged)")
|
||||||
|
logger.info(f" [FEA {trial_num}] Trefoil RMS: {objectives['trefoil_rms_nm']:.2f} nm (logged)")
|
||||||
|
logger.info(f" [FEA {trial_num}] Spher RMS: {objectives['spherical_rms_nm']:.2f} nm (logged)")
|
||||||
|
logger.info(f" [FEA {trial_num}] R² fit: {objectives['linear_fit_r2']:.4f} (physics validation)")
|
||||||
|
logger.info(f" [FEA {trial_num}] Mass: {objectives['mass_kg']:.3f} kg [Constraint: {constraint_status}]")
|
||||||
|
logger.info(f" [FEA {trial_num}] Weighted Sum: {weighted_sum:.2f}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'trial_num': trial_num,
|
||||||
|
'params': params,
|
||||||
|
'objectives': objectives,
|
||||||
|
'weighted_sum': weighted_sum,
|
||||||
|
'is_feasible': is_feasible,
|
||||||
|
'constraint_violation': violation,
|
||||||
|
'source': 'FEA_ZernikeTrajectory',
|
||||||
|
'solve_time': solve_time,
|
||||||
|
'iter_folder': str(iter_folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" [FEA {trial_num}] Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _extract_objectives_trajectory(self, op2_path: Path, iter_folder: Path) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Extract objectives using Zernike Trajectory Method.
|
||||||
|
|
||||||
|
Analyzes 5 elevation angles (20, 30, 40, 50, 60 deg) and provides:
|
||||||
|
- total_filtered_rms_nm: Integrated RMS across operating range
|
||||||
|
- Mode-specific RMS: coma, astigmatism, trefoil, spherical
|
||||||
|
- linear_fit_r2: Physics model validation
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Run trajectory extraction
|
||||||
|
result = extract_zernike_trajectory(
|
||||||
|
op2_file=op2_path,
|
||||||
|
reference_angle=REFERENCE_ANGLE,
|
||||||
|
focal_length=FOCAL_LENGTH,
|
||||||
|
unit='mm'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract mass from temp file
|
||||||
|
mass_kg = 0.0
|
||||||
|
mass_file = iter_folder / "_temp_mass.txt"
|
||||||
|
if mass_file.exists():
|
||||||
|
try:
|
||||||
|
with open(mass_file, 'r') as f:
|
||||||
|
mass_kg = float(f.read().strip())
|
||||||
|
except Exception as mass_err:
|
||||||
|
logger.warning(f" Could not read mass file: {mass_err}")
|
||||||
|
|
||||||
|
if mass_kg == 0:
|
||||||
|
props_file = iter_folder / "_temp_part_properties.json"
|
||||||
|
if props_file.exists():
|
||||||
|
try:
|
||||||
|
with open(props_file, 'r') as f:
|
||||||
|
props = json.load(f)
|
||||||
|
mass_kg = props.get('mass_kg', 0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
objectives = {
|
||||||
|
'total_filtered_rms_nm': result['total_filtered_rms_nm'],
|
||||||
|
'coma_rms_nm': result['coma_rms_nm'],
|
||||||
|
'astigmatism_rms_nm': result['astigmatism_rms_nm'],
|
||||||
|
'trefoil_rms_nm': result['trefoil_rms_nm'],
|
||||||
|
'spherical_rms_nm': result['spherical_rms_nm'],
|
||||||
|
'linear_fit_r2': result['linear_fit_r2'],
|
||||||
|
'mass_kg': mass_kg
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectives
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Trajectory extraction failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TPE Optimizer
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class TPEOptimizer:
|
||||||
|
"""TPE optimizer for trajectory-based optimization."""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any], resume: bool = False):
|
||||||
|
self.config = config
|
||||||
|
self.resume = resume
|
||||||
|
self.fea_runner = FEARunner(config)
|
||||||
|
|
||||||
|
# Load design variable bounds
|
||||||
|
self.design_vars = {
|
||||||
|
v['name']: {'min': v['min'], 'max': v['max'], 'baseline': v.get('baseline')}
|
||||||
|
for v in config['design_variables']
|
||||||
|
if v.get('enabled', True)
|
||||||
|
}
|
||||||
|
|
||||||
|
# TPE settings
|
||||||
|
opt_settings = config.get('optimization', {})
|
||||||
|
self.n_startup_trials = opt_settings.get('n_startup_trials', 15)
|
||||||
|
self.seed = opt_settings.get('seed', 42)
|
||||||
|
|
||||||
|
# Study
|
||||||
|
self.study_name = config.get('study_name', 'SAT3_Trajectory')
|
||||||
|
self.db_path = RESULTS_DIR / "study.db"
|
||||||
|
|
||||||
|
# Track best
|
||||||
|
self.best_weighted_sum = float('inf')
|
||||||
|
self.best_trial_info = None
|
||||||
|
|
||||||
|
# Track FEA count
|
||||||
|
self._count_existing_iterations()
|
||||||
|
|
||||||
|
def _count_existing_iterations(self):
|
||||||
|
"""Count existing iteration folders."""
|
||||||
|
self.fea_count = 0
|
||||||
|
if ITERATIONS_DIR.exists():
|
||||||
|
for d in ITERATIONS_DIR.iterdir():
|
||||||
|
if d.is_dir() and d.name.startswith('iter'):
|
||||||
|
self.fea_count += 1
|
||||||
|
logger.info(f"[INIT] Found {self.fea_count} existing FEA runs")
|
||||||
|
|
||||||
|
def objective_function(self, trial: optuna.Trial) -> float:
|
||||||
|
"""Optuna objective function."""
|
||||||
|
# Sample parameters
|
||||||
|
params = {}
|
||||||
|
for name, bounds in self.design_vars.items():
|
||||||
|
params[name] = trial.suggest_float(name, bounds['min'], bounds['max'])
|
||||||
|
|
||||||
|
# Run FEA
|
||||||
|
self.fea_count += 1
|
||||||
|
result = self.fea_runner.run_fea(params, self.fea_count)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
raise optuna.TrialPruned()
|
||||||
|
|
||||||
|
# Log all objectives (TPE only optimizes the return value, but we want all logged)
|
||||||
|
for obj_name, obj_value in result['objectives'].items():
|
||||||
|
trial.set_user_attr(obj_name, obj_value)
|
||||||
|
|
||||||
|
trial.set_user_attr('is_feasible', result['is_feasible'])
|
||||||
|
trial.set_user_attr('solve_time', result['solve_time'])
|
||||||
|
trial.set_user_attr('source', result['source'])
|
||||||
|
|
||||||
|
# Track best
|
||||||
|
if result['weighted_sum'] < self.best_weighted_sum:
|
||||||
|
self.best_weighted_sum = result['weighted_sum']
|
||||||
|
self.best_trial_info = result
|
||||||
|
logger.info(f" [NEW BEST] Trial {result['trial_num']}: {result['weighted_sum']:.2f}")
|
||||||
|
|
||||||
|
return result['weighted_sum']
|
||||||
|
|
||||||
|
def run(self, n_trials: int = 100):
|
||||||
|
"""Run TPE optimization."""
|
||||||
|
logger.info("="*80)
|
||||||
|
logger.info(f"Starting TPE Trajectory Optimization: {self.study_name}")
|
||||||
|
logger.info(f"Design Variables: {len(self.design_vars)}")
|
||||||
|
logger.info(f"n_trials: {n_trials}, n_startup_trials: {self.n_startup_trials}")
|
||||||
|
logger.info("="*80)
|
||||||
|
|
||||||
|
# Create or load study
|
||||||
|
storage = f"sqlite:///{self.db_path}"
|
||||||
|
if self.resume:
|
||||||
|
logger.info(f"[RESUME] Loading existing study from {self.db_path}")
|
||||||
|
study = optuna.load_study(study_name=self.study_name, storage=storage)
|
||||||
|
else:
|
||||||
|
logger.info(f"[NEW] Creating new study at {self.db_path}")
|
||||||
|
sampler = TPESampler(
|
||||||
|
n_startup_trials=self.n_startup_trials,
|
||||||
|
seed=self.seed
|
||||||
|
)
|
||||||
|
study = optuna.create_study(
|
||||||
|
study_name=self.study_name,
|
||||||
|
storage=storage,
|
||||||
|
sampler=sampler,
|
||||||
|
direction='minimize',
|
||||||
|
load_if_exists=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run optimization
|
||||||
|
study.optimize(self.objective_function, n_trials=n_trials)
|
||||||
|
|
||||||
|
logger.info("="*80)
|
||||||
|
logger.info(f"Optimization Complete!")
|
||||||
|
logger.info(f"Best weighted sum: {self.best_weighted_sum:.2f}")
|
||||||
|
if self.best_trial_info:
|
||||||
|
logger.info(f"Best objectives:")
|
||||||
|
for k, v in self.best_trial_info['objectives'].items():
|
||||||
|
logger.info(f" {k}: {v:.3f}")
|
||||||
|
logger.info("="*80)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Entry Point
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="SAT3 Trajectory Optimization (TPE)")
|
||||||
|
parser.add_argument('--start', action='store_true', help='Start optimization')
|
||||||
|
parser.add_argument('--trials', type=int, default=100, help='Number of trials (default: 100)')
|
||||||
|
parser.add_argument('--resume', action='store_true', help='Resume existing study')
|
||||||
|
parser.add_argument('--test', action='store_true', help='Run single FEA test')
|
||||||
|
parser.add_argument('--no-dashboard', action='store_true', help="Don't auto-launch dashboard")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.start:
|
||||||
|
# Launch dashboard unless disabled
|
||||||
|
if not args.no_dashboard:
|
||||||
|
launch_dashboard()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
optimizer = TPEOptimizer(CONFIG, resume=args.resume)
|
||||||
|
optimizer.run(n_trials=args.trials)
|
||||||
|
|
||||||
|
elif args.test:
|
||||||
|
logger.info("[TEST] Running single FEA trial...")
|
||||||
|
runner = FEARunner(CONFIG)
|
||||||
|
|
||||||
|
# Use baseline values
|
||||||
|
params = {v['name']: v['baseline'] for v in CONFIG['design_variables'] if v.get('enabled', True)}
|
||||||
|
|
||||||
|
result = runner.run_fea(params, trial_num=0)
|
||||||
|
if result:
|
||||||
|
logger.info("[TEST] Success!")
|
||||||
|
else:
|
||||||
|
logger.error("[TEST] Failed")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
48
test_trajectory_extractor.py
Normal file
48
test_trajectory_extractor.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""Test the Zernike Trajectory extractor on an existing OP2 file."""
|
||||||
|
|
||||||
|
from optimization_engine.extractors.extract_zernike_trajectory import extract_zernike_trajectory
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
op2_file = Path(r'tests\M1_Tensor\Atomizer_M1_Best_2026-01-29 - Tensor\assy_m1_assyfem1_sim1-solution_1.op2')
|
||||||
|
|
||||||
|
print(f'Testing trajectory extractor on: {op2_file}')
|
||||||
|
print('=' * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = extract_zernike_trajectory(
|
||||||
|
op2_file,
|
||||||
|
reference_angle=20.0,
|
||||||
|
focal_length=22000.0
|
||||||
|
)
|
||||||
|
|
||||||
|
print('[OK] Extractor ran successfully!')
|
||||||
|
print()
|
||||||
|
print(f'Angles detected: {result["angles_deg"]}')
|
||||||
|
print(f'Reference angle: {result["reference_angle"]} deg')
|
||||||
|
print(f'Number of angles: {result["n_angles"]}')
|
||||||
|
print()
|
||||||
|
print(f'Linear fit R2: {result["linear_fit_r2"]:.4f}')
|
||||||
|
if result["linear_fit_r2"] > 0.95:
|
||||||
|
print(' [OK] Excellent fit - physics model validated')
|
||||||
|
elif result["linear_fit_r2"] > 0.85:
|
||||||
|
print(' [~] Good fit - some nonlinearity present')
|
||||||
|
else:
|
||||||
|
print(' [!] Poor fit - significant nonlinearity')
|
||||||
|
print()
|
||||||
|
print('--- Mode-Specific RMS (nm) ---')
|
||||||
|
print(f'Total Filtered RMS: {result["total_filtered_rms_nm"]:.2f} nm')
|
||||||
|
print(f'Coma RMS: {result["coma_rms_nm"]:.2f} nm')
|
||||||
|
print(f'Astigmatism RMS: {result["astigmatism_rms_nm"]:.2f} nm')
|
||||||
|
print(f'Trefoil RMS: {result["trefoil_rms_nm"]:.2f} nm')
|
||||||
|
print(f'Spherical RMS: {result["spherical_rms_nm"]:.2f} nm')
|
||||||
|
print()
|
||||||
|
print(f'Dominant mode: {result["dominant_mode"]}')
|
||||||
|
print(f'Mode ranking: {", ".join(result["mode_ranking"][:5])}')
|
||||||
|
print()
|
||||||
|
print('[OK] All validation checks passed!')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR] {e}')
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
exit(1)
|
||||||
Reference in New Issue
Block a user