feat: Add Zernike wavefront viewer and V14 TPE optimization study
Dashboard Zernike Analysis:
- Add ZernikeViewer component with tabbed UI (40°, 60°, 90° vs 20°)
- Generate 3D surface mesh plots with Mesh3d triangulation
- Full 50-mode Zernike coefficient tables with mode names
- Manufacturing metrics for 90_vs_20 (optician workload analysis)
- OP2 availability filter for FEA trials only
- Fix duplicate trial display with unique React keys
- Tab switching with proper event propagation
Backend API Enhancements:
- GET /studies/{id}/trials/{num}/zernike - Generate Zernike HTML on-demand
- GET /studies/{id}/zernike-available - List trials with OP2 files
- compute_manufacturing_metrics() for aberration analysis
- compute_rms_filter_j1to3() for optician workload metric
M1 Mirror V14 Study:
- TPE (Tree-structured Parzen Estimator) optimization
- Seeds from 496 prior FEA trials (V11+V12+V13)
- Weighted-sum objective: 5*obj_40 + 5*obj_60 + 1*obj_mfg
- Multivariate TPE with constant_liar for efficient exploration
- Ready for 8-hour overnight runs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
198
studies/m1_mirror_adaptive_V14/1_setup/optimization_config.json
Normal file
198
studies/m1_mirror_adaptive_V14/1_setup/optimization_config.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"$schema": "Atomizer M1 Mirror TPE Optimization V14",
|
||||
"study_name": "m1_mirror_adaptive_V14",
|
||||
"description": "V14 - TPE single-objective optimization seeded from V11+V12+V13 FEA trials. Weighted-sum objective for efficient convergence.",
|
||||
|
||||
"source_studies": {
|
||||
"v11": {
|
||||
"database": "../m1_mirror_adaptive_V11/3_results/study.db",
|
||||
"description": "V11 FEA trials (107 from V10 + V11)"
|
||||
},
|
||||
"v12": {
|
||||
"database": "../m1_mirror_adaptive_V12/3_results/study.db",
|
||||
"description": "V12 FEA trials from GNN validation"
|
||||
},
|
||||
"v13": {
|
||||
"database": "../m1_mirror_adaptive_V13/3_results/study.db",
|
||||
"description": "V13 FEA trials from NSGA-II run"
|
||||
}
|
||||
},
|
||||
|
||||
"design_variables": [
|
||||
{
|
||||
"name": "lateral_inner_angle",
|
||||
"expression_name": "lateral_inner_angle",
|
||||
"min": 25.0,
|
||||
"max": 28.5,
|
||||
"baseline": 26.79,
|
||||
"units": "degrees",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "lateral_outer_angle",
|
||||
"expression_name": "lateral_outer_angle",
|
||||
"min": 13.0,
|
||||
"max": 17.0,
|
||||
"baseline": 14.64,
|
||||
"units": "degrees",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "lateral_outer_pivot",
|
||||
"expression_name": "lateral_outer_pivot",
|
||||
"min": 9.0,
|
||||
"max": 12.0,
|
||||
"baseline": 10.40,
|
||||
"units": "mm",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "lateral_inner_pivot",
|
||||
"expression_name": "lateral_inner_pivot",
|
||||
"min": 9.0,
|
||||
"max": 12.0,
|
||||
"baseline": 10.07,
|
||||
"units": "mm",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "lateral_middle_pivot",
|
||||
"expression_name": "lateral_middle_pivot",
|
||||
"min": 18.0,
|
||||
"max": 23.0,
|
||||
"baseline": 20.73,
|
||||
"units": "mm",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "lateral_closeness",
|
||||
"expression_name": "lateral_closeness",
|
||||
"min": 9.5,
|
||||
"max": 12.5,
|
||||
"baseline": 11.02,
|
||||
"units": "mm",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "whiffle_min",
|
||||
"expression_name": "whiffle_min",
|
||||
"min": 35.0,
|
||||
"max": 55.0,
|
||||
"baseline": 40.55,
|
||||
"units": "mm",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "whiffle_outer_to_vertical",
|
||||
"expression_name": "whiffle_outer_to_vertical",
|
||||
"min": 68.0,
|
||||
"max": 80.0,
|
||||
"baseline": 75.67,
|
||||
"units": "degrees",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "whiffle_triangle_closeness",
|
||||
"expression_name": "whiffle_triangle_closeness",
|
||||
"min": 50.0,
|
||||
"max": 65.0,
|
||||
"baseline": 60.00,
|
||||
"units": "mm",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "blank_backface_angle",
|
||||
"expression_name": "blank_backface_angle",
|
||||
"min": 4,
|
||||
"max": 5.0,
|
||||
"baseline": 4.23,
|
||||
"units": "degrees",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "inner_circular_rib_dia",
|
||||
"expression_name": "inner_circular_rib_dia",
|
||||
"min": 480.0,
|
||||
"max": 620.0,
|
||||
"baseline": 534.00,
|
||||
"units": "mm",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
|
||||
"objectives": [
|
||||
{
|
||||
"name": "rel_filtered_rms_40_vs_20",
|
||||
"description": "Filtered RMS WFE at 40 deg relative to 20 deg reference (operational tracking)",
|
||||
"direction": "minimize",
|
||||
"weight": 5.0,
|
||||
"target": 4.0,
|
||||
"units": "nm",
|
||||
"extractor_config": {
|
||||
"target_subcase": "3",
|
||||
"reference_subcase": "2",
|
||||
"metric": "relative_filtered_rms_nm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rel_filtered_rms_60_vs_20",
|
||||
"description": "Filtered RMS WFE at 60 deg relative to 20 deg reference (operational tracking)",
|
||||
"direction": "minimize",
|
||||
"weight": 5.0,
|
||||
"target": 10.0,
|
||||
"units": "nm",
|
||||
"extractor_config": {
|
||||
"target_subcase": "4",
|
||||
"reference_subcase": "2",
|
||||
"metric": "relative_filtered_rms_nm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mfg_90_optician_workload",
|
||||
"description": "Manufacturing deformation at 90 deg polishing (J1-J3 filtered RMS)",
|
||||
"direction": "minimize",
|
||||
"weight": 1.0,
|
||||
"target": 20.0,
|
||||
"units": "nm",
|
||||
"extractor_config": {
|
||||
"target_subcase": "1",
|
||||
"reference_subcase": "2",
|
||||
"metric": "relative_rms_filter_j1to3"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"zernike_settings": {
|
||||
"n_modes": 50,
|
||||
"filter_low_orders": 4,
|
||||
"displacement_unit": "mm",
|
||||
"subcases": ["1", "2", "3", "4"],
|
||||
"subcase_labels": {"1": "90deg", "2": "20deg", "3": "40deg", "4": "60deg"},
|
||||
"reference_subcase": "2"
|
||||
},
|
||||
|
||||
"tpe_settings": {
|
||||
"n_startup_trials": 10,
|
||||
"n_ei_candidates": 24,
|
||||
"multivariate": true,
|
||||
"constant_liar": true,
|
||||
"seed": 42
|
||||
},
|
||||
|
||||
"nx_settings": {
|
||||
"nx_install_path": "C:\\Program Files\\Siemens\\NX2506",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
639
studies/m1_mirror_adaptive_V14/run_optimization.py
Normal file
639
studies/m1_mirror_adaptive_V14/run_optimization.py
Normal file
@@ -0,0 +1,639 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
M1 Mirror TPE FEA Optimization V14
|
||||
===================================
|
||||
|
||||
TPE (Tree-structured Parzen Estimator) optimization seeded from V11+V12+V13.
|
||||
Uses weighted-sum objective for efficient single-objective convergence.
|
||||
|
||||
Key Features:
|
||||
1. TPE sampler - efficient Bayesian optimization for single objectives
|
||||
2. Seeds from all prior FEA trials (~150+ from V11, V12, V13)
|
||||
3. Weighted-sum objective: 5*obj_40 + 5*obj_60 + 1*obj_mfg
|
||||
4. Individual objectives tracked as user attributes
|
||||
|
||||
Usage:
|
||||
python run_optimization.py --start
|
||||
python run_optimization.py --start --trials 50
|
||||
python run_optimization.py --start --trials 50 --resume
|
||||
|
||||
For 8-hour overnight run (~55 trials at 8-9 min/trial):
|
||||
python run_optimization.py --start --trials 55
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
import logging
|
||||
import sqlite3
|
||||
import shutil
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
|
||||
# Add parent directories to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
import optuna
|
||||
from optuna.samplers import TPESampler
|
||||
|
||||
# Atomizer imports
|
||||
from optimization_engine.nx_solver import NXSolver
|
||||
from optimization_engine.utils import ensure_nx_running
|
||||
from optimization_engine.extractors import ZernikeExtractor
|
||||
|
||||
# ============================================================================
|
||||
# Paths
|
||||
# ============================================================================
|
||||
|
||||
STUDY_DIR = Path(__file__).parent
|
||||
SETUP_DIR = STUDY_DIR / "1_setup"
|
||||
ITERATIONS_DIR = STUDY_DIR / "2_iterations"
|
||||
RESULTS_DIR = STUDY_DIR / "3_results"
|
||||
CONFIG_PATH = SETUP_DIR / "optimization_config.json"
|
||||
|
||||
# Source studies for seeding
|
||||
V11_DB = STUDY_DIR.parent / "m1_mirror_adaptive_V11" / "3_results" / "study.db"
|
||||
V12_DB = STUDY_DIR.parent / "m1_mirror_adaptive_V12" / "3_results" / "study.db"
|
||||
V13_DB = STUDY_DIR.parent / "m1_mirror_adaptive_V13" / "3_results" / "study.db"
|
||||
|
||||
# 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__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Objective names and weights
|
||||
# ============================================================================
|
||||
|
||||
OBJ_NAMES = [
|
||||
'rel_filtered_rms_40_vs_20',
|
||||
'rel_filtered_rms_60_vs_20',
|
||||
'mfg_90_optician_workload'
|
||||
]
|
||||
|
||||
# Weights for weighted-sum objective (from config)
|
||||
OBJ_WEIGHTS = {
|
||||
'rel_filtered_rms_40_vs_20': 5.0,
|
||||
'rel_filtered_rms_60_vs_20': 5.0,
|
||||
'mfg_90_optician_workload': 1.0
|
||||
}
|
||||
|
||||
DESIGN_VAR_NAMES = [
|
||||
'lateral_inner_angle', 'lateral_outer_angle', 'lateral_outer_pivot',
|
||||
'lateral_inner_pivot', 'lateral_middle_pivot', 'lateral_closeness',
|
||||
'whiffle_min', 'whiffle_outer_to_vertical', 'whiffle_triangle_closeness',
|
||||
'blank_backface_angle', 'inner_circular_rib_dia'
|
||||
]
|
||||
|
||||
|
||||
def compute_weighted_sum(objectives: Dict[str, float]) -> float:
|
||||
"""Compute weighted sum of objectives."""
|
||||
total = 0.0
|
||||
for name, weight in OBJ_WEIGHTS.items():
|
||||
total += weight * objectives.get(name, 1000.0)
|
||||
return total
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Prior Data Loader
|
||||
# ============================================================================
|
||||
|
||||
def load_fea_trials_from_db(db_path: Path, label: str) -> List[Dict]:
|
||||
"""Load FEA trials from an Optuna database."""
|
||||
if not db_path.exists():
|
||||
logger.warning(f"{label} database not found: {db_path}")
|
||||
return []
|
||||
|
||||
fea_data = []
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT trial_id, number FROM trials
|
||||
WHERE state = 'COMPLETE'
|
||||
''')
|
||||
trials = cursor.fetchall()
|
||||
|
||||
for trial_id, trial_num in trials:
|
||||
# Get user attributes
|
||||
cursor.execute('''
|
||||
SELECT key, value_json FROM trial_user_attributes
|
||||
WHERE trial_id = ?
|
||||
''', (trial_id,))
|
||||
attrs = {row[0]: json.loads(row[1]) for row in cursor.fetchall()}
|
||||
|
||||
# Check if FEA trial (source contains 'FEA')
|
||||
source = attrs.get('source', 'FEA')
|
||||
if isinstance(source, str) and 'FEA' not in source:
|
||||
continue # Skip NN trials
|
||||
|
||||
# Get params
|
||||
cursor.execute('''
|
||||
SELECT param_name, param_value FROM trial_params
|
||||
WHERE trial_id = ?
|
||||
''', (trial_id,))
|
||||
params = {name: float(value) for name, value in cursor.fetchall()}
|
||||
|
||||
if not params:
|
||||
continue
|
||||
|
||||
# Get objectives (stored as individual attributes or in 'objectives')
|
||||
objectives = {}
|
||||
if 'objectives' in attrs:
|
||||
objectives = attrs['objectives']
|
||||
else:
|
||||
# Try individual attributes
|
||||
for obj_name in OBJ_NAMES:
|
||||
if obj_name in attrs:
|
||||
objectives[obj_name] = attrs[obj_name]
|
||||
|
||||
if all(k in objectives for k in OBJ_NAMES):
|
||||
fea_data.append({
|
||||
'trial_num': trial_num,
|
||||
'params': params,
|
||||
'objectives': objectives,
|
||||
'source': f'{label}_{source}'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading {label} data: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
logger.info(f"Loaded {len(fea_data)} FEA trials from {label}")
|
||||
return fea_data
|
||||
|
||||
|
||||
def load_all_prior_fea_data() -> List[Dict]:
|
||||
"""Load FEA trials from V11, V12, and V13."""
|
||||
all_data = []
|
||||
|
||||
# V11 data
|
||||
v11_data = load_fea_trials_from_db(V11_DB, "V11")
|
||||
all_data.extend(v11_data)
|
||||
|
||||
# V12 data
|
||||
v12_data = load_fea_trials_from_db(V12_DB, "V12")
|
||||
all_data.extend(v12_data)
|
||||
|
||||
# V13 data
|
||||
v13_data = load_fea_trials_from_db(V13_DB, "V13")
|
||||
all_data.extend(v13_data)
|
||||
|
||||
logger.info(f"Total prior FEA trials: {len(all_data)}")
|
||||
return all_data
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# FEA Runner
|
||||
# ============================================================================
|
||||
|
||||
class FEARunner:
|
||||
"""Runs actual FEA simulations."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.config = config
|
||||
self.nx_solver = None
|
||||
self.nx_manager = None
|
||||
self.master_model_dir = SETUP_DIR / "model"
|
||||
|
||||
def setup(self):
|
||||
"""Setup NX and solver."""
|
||||
logger.info("Setting up NX session...")
|
||||
|
||||
study_name = self.config.get('study_name', 'm1_mirror_adaptive_V14')
|
||||
|
||||
try:
|
||||
self.nx_manager, nx_was_started = ensure_nx_running(
|
||||
session_id=study_name,
|
||||
auto_start=True,
|
||||
start_timeout=120
|
||||
)
|
||||
logger.info("NX session ready" + (" (started)" if nx_was_started else " (existing)"))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to setup NX: {e}")
|
||||
raise
|
||||
|
||||
# Initialize solver
|
||||
nx_settings = self.config.get('nx_settings', {})
|
||||
nx_install_dir = nx_settings.get('nx_install_path', 'C:\\Program Files\\Siemens\\NX2506')
|
||||
version_match = re.search(r'NX(\d+)', nx_install_dir)
|
||||
nastran_version = version_match.group(1) if version_match else "2506"
|
||||
|
||||
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="m1_mirror_adaptive_V14"
|
||||
)
|
||||
|
||||
def run_fea(self, params: Dict[str, float], trial_num: int) -> Optional[Dict]:
|
||||
"""Run FEA and extract objectives."""
|
||||
if self.nx_solver is None:
|
||||
self.setup()
|
||||
|
||||
logger.info(f" [FEA {trial_num}] Running simulation...")
|
||||
|
||||
expressions = {var['expression_name']: params[var['name']]
|
||||
for var in self.config['design_variables']}
|
||||
|
||||
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
|
||||
op2_path = Path(result['op2_file'])
|
||||
objectives = self._extract_objectives(op2_path)
|
||||
|
||||
if objectives is None:
|
||||
return None
|
||||
|
||||
weighted_sum = compute_weighted_sum(objectives)
|
||||
|
||||
logger.info(f" [FEA {trial_num}] 40-20: {objectives['rel_filtered_rms_40_vs_20']:.2f} nm")
|
||||
logger.info(f" [FEA {trial_num}] 60-20: {objectives['rel_filtered_rms_60_vs_20']:.2f} nm")
|
||||
logger.info(f" [FEA {trial_num}] Mfg: {objectives['mfg_90_optician_workload']:.2f} nm")
|
||||
logger.info(f" [FEA {trial_num}] Weighted Sum: {weighted_sum:.2f}")
|
||||
|
||||
return {
|
||||
'trial_num': trial_num,
|
||||
'params': params,
|
||||
'objectives': objectives,
|
||||
'weighted_sum': weighted_sum,
|
||||
'source': 'FEA',
|
||||
'solve_time': solve_time
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" [FEA {trial_num}] Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _extract_objectives(self, op2_path: Path) -> Optional[Dict[str, float]]:
|
||||
"""Extract objectives using ZernikeExtractor."""
|
||||
try:
|
||||
zernike_settings = self.config.get('zernike_settings', {})
|
||||
|
||||
extractor = ZernikeExtractor(
|
||||
op2_path,
|
||||
bdf_path=None,
|
||||
displacement_unit=zernike_settings.get('displacement_unit', 'mm'),
|
||||
n_modes=zernike_settings.get('n_modes', 50),
|
||||
filter_orders=zernike_settings.get('filter_low_orders', 4)
|
||||
)
|
||||
|
||||
ref = zernike_settings.get('reference_subcase', '2')
|
||||
|
||||
rel_40 = extractor.extract_relative("3", ref)
|
||||
rel_60 = extractor.extract_relative("4", ref)
|
||||
rel_90 = extractor.extract_relative("1", ref)
|
||||
|
||||
return {
|
||||
'rel_filtered_rms_40_vs_20': rel_40['relative_filtered_rms_nm'],
|
||||
'rel_filtered_rms_60_vs_20': rel_60['relative_filtered_rms_nm'],
|
||||
'mfg_90_optician_workload': rel_90['relative_rms_filter_j1to3']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Zernike extraction failed: {e}")
|
||||
return None
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup NX session."""
|
||||
if self.nx_manager:
|
||||
if self.nx_manager.can_close_nx():
|
||||
self.nx_manager.close_nx_if_allowed()
|
||||
self.nx_manager.cleanup()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TPE Optimizer
|
||||
# ============================================================================
|
||||
|
||||
class TPEOptimizer:
|
||||
"""TPE-based FEA optimizer with weighted-sum objective."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.config = config
|
||||
self.fea_runner = FEARunner(config)
|
||||
|
||||
# Load prior data for seeding
|
||||
self.prior_data = load_all_prior_fea_data()
|
||||
|
||||
# Database
|
||||
self.db_path = RESULTS_DIR / "study.db"
|
||||
self.storage = optuna.storages.RDBStorage(f'sqlite:///{self.db_path}')
|
||||
|
||||
# State
|
||||
self.trial_count = 0
|
||||
self.best_value = float('inf')
|
||||
self.best_trial = None
|
||||
|
||||
def _get_next_trial_number(self) -> int:
|
||||
"""Get the next trial number based on existing iterations."""
|
||||
existing = list(ITERATIONS_DIR.glob("iter*"))
|
||||
if not existing:
|
||||
return 1
|
||||
max_num = max(int(p.name.replace("iter", "")) for p in existing)
|
||||
return max_num + 1
|
||||
|
||||
def seed_from_prior(self, study: optuna.Study):
|
||||
"""Seed the study with prior FEA trials."""
|
||||
if not self.prior_data:
|
||||
logger.warning("No prior data to seed from")
|
||||
return
|
||||
|
||||
logger.info(f"Seeding study with {len(self.prior_data)} prior FEA trials...")
|
||||
|
||||
for i, d in enumerate(self.prior_data):
|
||||
try:
|
||||
# Create a trial with the prior data
|
||||
distributions = {}
|
||||
for var in self.config['design_variables']:
|
||||
if var.get('enabled', False):
|
||||
distributions[var['name']] = optuna.distributions.FloatDistribution(
|
||||
var['min'], var['max']
|
||||
)
|
||||
|
||||
# Compute weighted sum for the prior trial
|
||||
weighted_sum = compute_weighted_sum(d['objectives'])
|
||||
|
||||
# Create frozen trial
|
||||
frozen_trial = optuna.trial.create_trial(
|
||||
params=d['params'],
|
||||
distributions=distributions,
|
||||
values=[weighted_sum], # Single objective for TPE
|
||||
user_attrs={
|
||||
'source': d.get('source', 'prior_FEA'),
|
||||
'rel_filtered_rms_40_vs_20': d['objectives']['rel_filtered_rms_40_vs_20'],
|
||||
'rel_filtered_rms_60_vs_20': d['objectives']['rel_filtered_rms_60_vs_20'],
|
||||
'mfg_90_optician_workload': d['objectives']['mfg_90_optician_workload'],
|
||||
'weighted_sum': weighted_sum,
|
||||
}
|
||||
)
|
||||
|
||||
study.add_trial(frozen_trial)
|
||||
|
||||
# Track best
|
||||
if weighted_sum < self.best_value:
|
||||
self.best_value = weighted_sum
|
||||
self.best_trial = d
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to seed trial {i}: {e}")
|
||||
|
||||
logger.info(f"Seeded {len(study.trials)} trials")
|
||||
if self.best_trial:
|
||||
logger.info(f"Best prior: weighted_sum={self.best_value:.2f}")
|
||||
logger.info(f" 40-20: {self.best_trial['objectives']['rel_filtered_rms_40_vs_20']:.2f} nm")
|
||||
logger.info(f" 60-20: {self.best_trial['objectives']['rel_filtered_rms_60_vs_20']:.2f} nm")
|
||||
logger.info(f" Mfg: {self.best_trial['objectives']['mfg_90_optician_workload']:.2f} nm")
|
||||
|
||||
def run(self, n_trials: int = 50, resume: bool = False):
|
||||
"""Run TPE optimization."""
|
||||
logger.info("\n" + "=" * 70)
|
||||
logger.info("M1 MIRROR TPE FEA OPTIMIZATION V14")
|
||||
logger.info("=" * 70)
|
||||
logger.info(f"Prior FEA trials: {len(self.prior_data)}")
|
||||
logger.info(f"New trials to run: {n_trials}")
|
||||
logger.info(f"Objectives: {OBJ_NAMES}")
|
||||
logger.info(f"Weights: {OBJ_WEIGHTS}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Create or load study with TPE sampler
|
||||
tpe_settings = self.config.get('tpe_settings', {})
|
||||
sampler = TPESampler(
|
||||
n_startup_trials=tpe_settings.get('n_startup_trials', 10),
|
||||
n_ei_candidates=tpe_settings.get('n_ei_candidates', 24),
|
||||
multivariate=tpe_settings.get('multivariate', True),
|
||||
constant_liar=tpe_settings.get('constant_liar', True),
|
||||
seed=tpe_settings.get('seed', 42)
|
||||
)
|
||||
|
||||
study = optuna.create_study(
|
||||
study_name="v14_tpe",
|
||||
storage=self.storage,
|
||||
direction='minimize', # Single objective - minimize weighted sum
|
||||
sampler=sampler,
|
||||
load_if_exists=resume
|
||||
)
|
||||
|
||||
# Seed with prior data if starting fresh
|
||||
if not resume or len(study.trials) == 0:
|
||||
self.seed_from_prior(study)
|
||||
|
||||
self.trial_count = self._get_next_trial_number()
|
||||
logger.info(f"Starting from trial {self.trial_count}")
|
||||
|
||||
# Run optimization
|
||||
def objective(trial: optuna.Trial) -> float:
|
||||
# Sample parameters
|
||||
params = {}
|
||||
for var in self.config['design_variables']:
|
||||
if var.get('enabled', False):
|
||||
params[var['name']] = trial.suggest_float(var['name'], var['min'], var['max'])
|
||||
|
||||
# Run FEA
|
||||
result = self.fea_runner.run_fea(params, self.trial_count)
|
||||
self.trial_count += 1
|
||||
|
||||
if result is None:
|
||||
# Return worst-case value for failed trials
|
||||
return 10000.0
|
||||
|
||||
# Store objectives as user attributes
|
||||
trial.set_user_attr('source', 'FEA')
|
||||
trial.set_user_attr('rel_filtered_rms_40_vs_20', result['objectives']['rel_filtered_rms_40_vs_20'])
|
||||
trial.set_user_attr('rel_filtered_rms_60_vs_20', result['objectives']['rel_filtered_rms_60_vs_20'])
|
||||
trial.set_user_attr('mfg_90_optician_workload', result['objectives']['mfg_90_optician_workload'])
|
||||
trial.set_user_attr('weighted_sum', result['weighted_sum'])
|
||||
trial.set_user_attr('solve_time', result.get('solve_time', 0))
|
||||
|
||||
# Track best
|
||||
if result['weighted_sum'] < self.best_value:
|
||||
self.best_value = result['weighted_sum']
|
||||
logger.info(f" [NEW BEST] Weighted Sum: {self.best_value:.2f}")
|
||||
|
||||
return result['weighted_sum']
|
||||
|
||||
# Run
|
||||
try:
|
||||
study.optimize(
|
||||
objective,
|
||||
n_trials=n_trials,
|
||||
show_progress_bar=True,
|
||||
gc_after_trial=True
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("\nOptimization interrupted by user")
|
||||
finally:
|
||||
self.fea_runner.cleanup()
|
||||
|
||||
# Print results
|
||||
elapsed = time.time() - start_time
|
||||
self._print_results(study, elapsed)
|
||||
|
||||
def _print_results(self, study: optuna.Study, elapsed: float):
|
||||
"""Print optimization results."""
|
||||
logger.info("\n" + "=" * 70)
|
||||
logger.info("OPTIMIZATION COMPLETE")
|
||||
logger.info("=" * 70)
|
||||
logger.info(f"Time: {elapsed/60:.1f} min ({elapsed/3600:.2f} hours)")
|
||||
logger.info(f"Total trials: {len(study.trials)}")
|
||||
|
||||
# Get best trial
|
||||
best_trial = study.best_trial
|
||||
logger.info(f"\nBest Trial: #{best_trial.number}")
|
||||
logger.info(f" Weighted Sum: {best_trial.value:.2f}")
|
||||
logger.info(f" 40-20: {best_trial.user_attrs.get('rel_filtered_rms_40_vs_20', 'N/A'):.2f} nm")
|
||||
logger.info(f" 60-20: {best_trial.user_attrs.get('rel_filtered_rms_60_vs_20', 'N/A'):.2f} nm")
|
||||
logger.info(f" Mfg: {best_trial.user_attrs.get('mfg_90_optician_workload', 'N/A'):.2f} nm")
|
||||
|
||||
logger.info("\nBest Parameters:")
|
||||
for name, value in best_trial.params.items():
|
||||
logger.info(f" {name}: {value:.4f}")
|
||||
|
||||
# Find top 10 trials
|
||||
sorted_trials = sorted(
|
||||
[t for t in study.trials if t.value is not None and t.value < 10000],
|
||||
key=lambda t: t.value
|
||||
)[:10]
|
||||
|
||||
logger.info("\nTop 10 Trials:")
|
||||
logger.info("-" * 90)
|
||||
logger.info(f"{'#':>4} {'WeightedSum':>12} {'40-20 (nm)':>12} {'60-20 (nm)':>12} {'Mfg (nm)':>12} {'Source':>10}")
|
||||
logger.info("-" * 90)
|
||||
|
||||
for trial in sorted_trials:
|
||||
source = trial.user_attrs.get('source', 'unknown')[:10]
|
||||
logger.info(
|
||||
f"{trial.number:>4} "
|
||||
f"{trial.value:>12.2f} "
|
||||
f"{trial.user_attrs.get('rel_filtered_rms_40_vs_20', 0):>12.2f} "
|
||||
f"{trial.user_attrs.get('rel_filtered_rms_60_vs_20', 0):>12.2f} "
|
||||
f"{trial.user_attrs.get('mfg_90_optician_workload', 0):>12.2f} "
|
||||
f"{source:>10}"
|
||||
)
|
||||
|
||||
# Save results
|
||||
results = {
|
||||
'summary': {
|
||||
'total_trials': len(study.trials),
|
||||
'best_weighted_sum': best_trial.value,
|
||||
'elapsed_hours': elapsed / 3600
|
||||
},
|
||||
'best_trial': {
|
||||
'number': best_trial.number,
|
||||
'params': best_trial.params,
|
||||
'objectives': {
|
||||
'rel_filtered_rms_40_vs_20': best_trial.user_attrs.get('rel_filtered_rms_40_vs_20'),
|
||||
'rel_filtered_rms_60_vs_20': best_trial.user_attrs.get('rel_filtered_rms_60_vs_20'),
|
||||
'mfg_90_optician_workload': best_trial.user_attrs.get('mfg_90_optician_workload'),
|
||||
},
|
||||
'weighted_sum': best_trial.value
|
||||
},
|
||||
'top_10': [
|
||||
{
|
||||
'trial': t.number,
|
||||
'weighted_sum': t.value,
|
||||
'params': t.params,
|
||||
'objectives': {
|
||||
'rel_filtered_rms_40_vs_20': t.user_attrs.get('rel_filtered_rms_40_vs_20'),
|
||||
'rel_filtered_rms_60_vs_20': t.user_attrs.get('rel_filtered_rms_60_vs_20'),
|
||||
'mfg_90_optician_workload': t.user_attrs.get('mfg_90_optician_workload'),
|
||||
}
|
||||
}
|
||||
for t in sorted_trials
|
||||
]
|
||||
}
|
||||
|
||||
with open(RESULTS_DIR / 'final_results.json', 'w') as f:
|
||||
json.dump(results, f, indent=2)
|
||||
|
||||
logger.info(f"\nResults saved to {RESULTS_DIR / 'final_results.json'}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='M1 Mirror TPE V14')
|
||||
parser.add_argument('--start', action='store_true', help='Start optimization')
|
||||
parser.add_argument('--trials', type=int, default=50, help='Number of new FEA trials')
|
||||
parser.add_argument('--resume', action='store_true', help='Resume from existing study')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.start:
|
||||
print("M1 Mirror TPE FEA Optimization V14")
|
||||
print("=" * 50)
|
||||
print("\nUsage:")
|
||||
print(" python run_optimization.py --start")
|
||||
print(" python run_optimization.py --start --trials 55")
|
||||
print(" python run_optimization.py --start --trials 55 --resume")
|
||||
print("\nFor 8-hour overnight run (~55 trials at 8-9 min/trial):")
|
||||
print(" python run_optimization.py --start --trials 55")
|
||||
print("\nThis will:")
|
||||
print(" 1. Load FEA trials from V11, V12, V13 databases")
|
||||
print(" 2. Seed TPE with all prior FEA data")
|
||||
print(" 3. Run TPE optimization with weighted-sum objective")
|
||||
print(" 4. Weights: 5*obj_40 + 5*obj_60 + 1*obj_mfg")
|
||||
return
|
||||
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
optimizer = TPEOptimizer(config)
|
||||
optimizer.run(n_trials=args.trials, resume=args.resume)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user