diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1.prt b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1.prt new file mode 100644 index 00000000..45880bde Binary files /dev/null and b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1.prt differ diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1_assyfem1.afm b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1_assyfem1.afm new file mode 100644 index 00000000..0724789f Binary files /dev/null and b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1_assyfem1.afm differ diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1_assyfem1_sim1.sim b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1_assyfem1_sim1.sim new file mode 100644 index 00000000..6becb2ab Binary files /dev/null and b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/ASSY_M1_assyfem1_sim1.sim differ diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank.prt b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank.prt new file mode 100644 index 00000000..430bf9f9 Binary files /dev/null and b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank.prt differ diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank_fem1.fem b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank_fem1.fem new file mode 100644 index 00000000..2b0d2ab6 Binary files /dev/null and b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank_fem1.fem differ diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank_fem1_i.prt b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank_fem1_i.prt new file mode 100644 index 00000000..4b385bb8 Binary files /dev/null and b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Blank_fem1_i.prt differ diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Vertical_Support_Skeleton.prt b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Vertical_Support_Skeleton.prt new file mode 100644 index 00000000..347a59c1 Binary files /dev/null and b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Vertical_Support_Skeleton.prt differ diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Vertical_Support_Skeleton_fem1.fem b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Vertical_Support_Skeleton_fem1.fem new file mode 100644 index 00000000..70309fd2 Binary files /dev/null and b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Vertical_Support_Skeleton_fem1.fem differ diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Vertical_Support_Skeleton_fem1_i.prt b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Vertical_Support_Skeleton_fem1_i.prt new file mode 100644 index 00000000..6d4a0726 Binary files /dev/null and b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/M1_Vertical_Support_Skeleton_fem1_i.prt differ diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/params.exp b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/params.exp new file mode 100644 index 00000000..5f9983d6 --- /dev/null +++ b/studies/M1_Mirror/SAT3_Trajectory/1_setup/model/params.exp @@ -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 diff --git a/studies/M1_Mirror/SAT3_Trajectory/1_setup/optimization_config.json b/studies/M1_Mirror/SAT3_Trajectory/1_setup/optimization_config.json new file mode 100644 index 00000000..50fe837a --- /dev/null +++ b/studies/M1_Mirror/SAT3_Trajectory/1_setup/optimization_config.json @@ -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" + } +} diff --git a/studies/M1_Mirror/SAT3_Trajectory/README.md b/studies/M1_Mirror/SAT3_Trajectory/README.md new file mode 100644 index 00000000..db6efdd9 --- /dev/null +++ b/studies/M1_Mirror/SAT3_Trajectory/README.md @@ -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* diff --git a/studies/M1_Mirror/SAT3_Trajectory/STUDY_REPORT.md b/studies/M1_Mirror/SAT3_Trajectory/STUDY_REPORT.md new file mode 100644 index 00000000..bd3a843a --- /dev/null +++ b/studies/M1_Mirror/SAT3_Trajectory/STUDY_REPORT.md @@ -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* diff --git a/studies/M1_Mirror/SAT3_Trajectory/run_optimization.py b/studies/M1_Mirror/SAT3_Trajectory/run_optimization.py new file mode 100644 index 00000000..f8f8f412 --- /dev/null +++ b/studies/M1_Mirror/SAT3_Trajectory/run_optimization.py @@ -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() diff --git a/test_trajectory_extractor.py b/test_trajectory_extractor.py new file mode 100644 index 00000000..e1e36835 --- /dev/null +++ b/test_trajectory_extractor.py @@ -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)