feat: Add MLP surrogate with Turbo Mode for 100x faster optimization
Neural Acceleration (MLP Surrogate): - Add run_nn_optimization.py with hybrid FEA/NN workflow - MLP architecture: 4-layer (64->128->128->64) with BatchNorm/Dropout - Three workflow modes: - --all: Sequential export->train->optimize->validate - --hybrid-loop: Iterative Train->NN->Validate->Retrain cycle - --turbo: Aggressive single-best validation (RECOMMENDED) - Turbo mode: 5000 NN trials + 50 FEA validations in ~12 minutes - Separate nn_study.db to avoid overloading dashboard Performance Results (bracket_pareto_3obj study): - NN prediction errors: mass 1-5%, stress 1-4%, stiffness 5-15% - Found minimum mass designs at boundary (angle~30deg, thick~30mm) - 100x speedup vs pure FEA exploration Protocol Operating System: - Add .claude/skills/ with Bootstrap, Cheatsheet, Context Loader - Add docs/protocols/ with operations (OP_01-06) and system (SYS_10-14) - Update SYS_14_NEURAL_ACCELERATION.md with MLP Turbo Mode docs NX Automation: - Add optimization_engine/hooks/ for NX CAD/CAE automation - Add study_wizard.py for guided study creation - Fix FEM mesh update: load idealized part before UpdateFemodel() New Study: - bracket_pareto_3obj: 3-objective Pareto (mass, stress, stiffness) - 167 FEA trials + 5000 NN trials completed - Demonstrates full hybrid workflow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
245
studies/bracket_pareto_3obj/run_optimization.py
Normal file
245
studies/bracket_pareto_3obj/run_optimization.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
bracket_pareto_3obj - Optimization Script
|
||||
============================================================
|
||||
|
||||
Three-objective Pareto optimization: minimize mass, minimize stress, maximize stiffness
|
||||
|
||||
Protocol: Multi-Objective NSGA-II
|
||||
|
||||
Staged Workflow:
|
||||
----------------
|
||||
1. DISCOVER: python run_optimization.py --discover
|
||||
2. VALIDATE: python run_optimization.py --validate
|
||||
3. TEST: python run_optimization.py --test
|
||||
4. RUN: python run_optimization.py --run --trials 100
|
||||
|
||||
Generated by StudyWizard on 2025-12-06 14:43
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple, List
|
||||
|
||||
# Add parent directory to path
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
import optuna
|
||||
from optuna.samplers import NSGAIISampler
|
||||
|
||||
# Core imports
|
||||
from optimization_engine.nx_solver import NXSolver
|
||||
from optimization_engine.logger import get_logger
|
||||
|
||||
# Extractor imports
|
||||
from optimization_engine.extractors.bdf_mass_extractor import extract_mass_from_bdf
|
||||
from optimization_engine.extractors.extract_displacement import extract_displacement
|
||||
from optimization_engine.extractors.extract_von_mises_stress import extract_solid_stress
|
||||
|
||||
|
||||
def load_config(config_file: Path) -> dict:
|
||||
"""Load configuration from JSON file."""
|
||||
with open(config_file, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def clean_nastran_files(model_dir: Path, logger) -> List[Path]:
|
||||
"""Remove old Nastran solver output files."""
|
||||
patterns = ['*.op2', '*.f06', '*.log', '*.f04', '*.pch', '*.DBALL', '*.MASTER', '_temp*.txt']
|
||||
deleted = []
|
||||
|
||||
for pattern in patterns:
|
||||
for f in model_dir.glob(pattern):
|
||||
try:
|
||||
f.unlink()
|
||||
deleted.append(f)
|
||||
logger.info(f" Deleted: {f.name}")
|
||||
except Exception as e:
|
||||
logger.warning(f" Failed to delete {f.name}: {e}")
|
||||
|
||||
return deleted
|
||||
|
||||
|
||||
def objective(trial: optuna.Trial, config: dict, nx_solver: NXSolver,
|
||||
model_dir: Path, logger) -> Tuple[float, float, float]:
|
||||
"""
|
||||
Objective function for optimization.
|
||||
|
||||
Returns tuple of objectives for multi-objective optimization.
|
||||
"""
|
||||
# Sample design variables
|
||||
design_vars = {}
|
||||
for var in config['design_variables']:
|
||||
param_name = var['parameter']
|
||||
bounds = var['bounds']
|
||||
design_vars[param_name] = trial.suggest_float(param_name, bounds[0], bounds[1])
|
||||
|
||||
logger.trial_start(trial.number, design_vars)
|
||||
|
||||
try:
|
||||
# Get file paths
|
||||
sim_file = model_dir / config['simulation']['sim_file']
|
||||
|
||||
# Run FEA simulation
|
||||
result = nx_solver.run_simulation(
|
||||
sim_file=sim_file,
|
||||
working_dir=model_dir,
|
||||
expression_updates=design_vars,
|
||||
solution_name=config['simulation'].get('solution_name'),
|
||||
cleanup=True
|
||||
)
|
||||
|
||||
if not result['success']:
|
||||
logger.trial_failed(trial.number, f"Simulation failed: {result.get('error', 'Unknown')}")
|
||||
return (float('inf'), float('inf'), float('inf'))
|
||||
|
||||
op2_file = result['op2_file']
|
||||
dat_file = model_dir / config['simulation']['dat_file']
|
||||
|
||||
# Extract results
|
||||
obj_mass = extract_mass_from_bdf(str(dat_file))
|
||||
logger.info(f' mass: {obj_mass}')
|
||||
|
||||
stress_result = extract_solid_stress(op2_file, subcase=1, element_type='chexa')
|
||||
obj_stress = stress_result.get('max_von_mises', float('inf')) / 1000.0 # kPa -> MPa
|
||||
logger.info(f' stress: {obj_stress:.2f} MPa')
|
||||
|
||||
disp_result = extract_displacement(op2_file, subcase=1)
|
||||
max_displacement = disp_result['max_displacement']
|
||||
# For stiffness maximization, use inverse of displacement
|
||||
applied_force = 1000.0 # N - adjust based on your model
|
||||
obj_stiffness = -applied_force / max(abs(max_displacement), 1e-6)
|
||||
logger.info(f' stiffness: {obj_stiffness}')
|
||||
|
||||
|
||||
# Check constraints
|
||||
feasible = True
|
||||
constraint_results = {}
|
||||
# Check stress_limit (stress from OP2 is in kPa for mm/kg units, convert to MPa)
|
||||
const_stress_limit = extract_solid_stress(op2_file, element_type='chexa')
|
||||
stress_mpa = const_stress_limit.get('max_von_mises', float('inf')) / 1000.0 # kPa -> MPa
|
||||
constraint_results['stress_limit'] = stress_mpa
|
||||
if stress_mpa > 300:
|
||||
feasible = False
|
||||
logger.warning(f' Constraint violation: stress_limit = {stress_mpa:.1f} MPa vs 300 MPa')
|
||||
|
||||
|
||||
# Set user attributes
|
||||
trial.set_user_attr('mass', obj_mass)
|
||||
trial.set_user_attr('stress', obj_stress)
|
||||
trial.set_user_attr('stiffness', obj_stiffness)
|
||||
trial.set_user_attr('feasible', feasible)
|
||||
|
||||
objectives = {'mass': obj_mass, 'stress': obj_stress, 'stiffness': obj_stiffness}
|
||||
logger.trial_complete(trial.number, objectives, constraint_results, feasible)
|
||||
|
||||
return (obj_mass, obj_stress, obj_stiffness)
|
||||
|
||||
except Exception as e:
|
||||
logger.trial_failed(trial.number, str(e))
|
||||
return (float('inf'), float('inf'), float('inf'))
|
||||
|
||||
|
||||
def main():
|
||||
"""Main optimization workflow."""
|
||||
parser = argparse.ArgumentParser(description='bracket_pareto_3obj')
|
||||
|
||||
stage_group = parser.add_mutually_exclusive_group()
|
||||
stage_group.add_argument('--discover', action='store_true', help='Discover model outputs')
|
||||
stage_group.add_argument('--validate', action='store_true', help='Run single validation trial')
|
||||
stage_group.add_argument('--test', action='store_true', help='Run 3-trial test')
|
||||
stage_group.add_argument('--run', action='store_true', help='Run optimization')
|
||||
|
||||
parser.add_argument('--trials', type=int, default=100, help='Number of trials')
|
||||
parser.add_argument('--resume', action='store_true', help='Resume existing study')
|
||||
parser.add_argument('--clean', action='store_true', help='Clean old files first')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not any([args.discover, args.validate, args.test, args.run]):
|
||||
print("No stage specified. Use --discover, --validate, --test, or --run")
|
||||
return 1
|
||||
|
||||
# Setup paths
|
||||
study_dir = Path(__file__).parent
|
||||
config_path = study_dir / "1_setup" / "optimization_config.json"
|
||||
model_dir = study_dir / "1_setup" / "model"
|
||||
results_dir = study_dir / "2_results"
|
||||
results_dir.mkdir(exist_ok=True)
|
||||
|
||||
study_name = "bracket_pareto_3obj"
|
||||
|
||||
# Initialize
|
||||
logger = get_logger(study_name, study_dir=results_dir)
|
||||
config = load_config(config_path)
|
||||
nx_solver = NXSolver(nastran_version="2506")
|
||||
|
||||
if args.clean:
|
||||
clean_nastran_files(model_dir, logger)
|
||||
|
||||
# Run appropriate stage
|
||||
if args.discover or args.validate or args.test:
|
||||
# Run limited trials for these stages
|
||||
n = 1 if args.discover or args.validate else 3
|
||||
storage = f"sqlite:///{results_dir / 'study_test.db'}"
|
||||
|
||||
study = optuna.create_study(
|
||||
study_name=f"{study_name}_test",
|
||||
storage=storage,
|
||||
sampler=NSGAIISampler(population_size=5, seed=42),
|
||||
directions=['minimize'] * 3,
|
||||
load_if_exists=False
|
||||
)
|
||||
|
||||
study.optimize(
|
||||
lambda trial: objective(trial, config, nx_solver, model_dir, logger),
|
||||
n_trials=n,
|
||||
show_progress_bar=True
|
||||
)
|
||||
|
||||
logger.info(f"Completed {len(study.trials)} trial(s)")
|
||||
return 0
|
||||
|
||||
# Full optimization run
|
||||
storage = f"sqlite:///{results_dir / 'study.db'}"
|
||||
|
||||
if args.resume:
|
||||
study = optuna.load_study(
|
||||
study_name=study_name,
|
||||
storage=storage,
|
||||
sampler=NSGAIISampler(population_size=20, seed=42)
|
||||
)
|
||||
else:
|
||||
study = optuna.create_study(
|
||||
study_name=study_name,
|
||||
storage=storage,
|
||||
sampler=NSGAIISampler(population_size=20, seed=42),
|
||||
directions=['minimize'] * 3,
|
||||
load_if_exists=True
|
||||
)
|
||||
|
||||
logger.study_start(study_name, args.trials, "NSGAIISampler")
|
||||
|
||||
study.optimize(
|
||||
lambda trial: objective(trial, config, nx_solver, model_dir, logger),
|
||||
n_trials=args.trials,
|
||||
show_progress_bar=True
|
||||
)
|
||||
|
||||
n_complete = len([t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE])
|
||||
logger.study_complete(study_name, len(study.trials), n_complete)
|
||||
|
||||
# Report results
|
||||
pareto_trials = study.best_trials
|
||||
logger.info(f"\nOptimization Complete!")
|
||||
logger.info(f"Total trials: {len(study.trials)}")
|
||||
logger.info(f"Successful: {n_complete}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
Reference in New Issue
Block a user