Files
Atomizer/studies/Simple_Bracket/bracket_stiffness_optimization_V2/run_optimization.py
Anto01 73a7b9d9f1 feat: Add dashboard chat integration and MCP server
Major changes:
- Dashboard: WebSocket-based chat with session management
- Dashboard: New chat components (ChatPane, ChatInput, ModeToggle)
- Dashboard: Enhanced UI with parallel coordinates chart
- MCP Server: New atomizer-tools server for Claude integration
- Extractors: Enhanced Zernike OPD extractor
- Reports: Improved report generator

New studies (configs and scripts only):
- M1 Mirror: Cost reduction campaign studies
- Simple Beam, Simple Bracket, UAV Arm studies

Note: Large iteration data (2_iterations/, best_design_archive/)
excluded via .gitignore - kept on local Gitea only.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 15:53:55 -05:00

355 lines
13 KiB
Python

"""
Bracket Stiffness Optimization - Intelligent Optimizer (Protocol 10)
====================================================================
Run multi-objective optimization using Protocol 10: Intelligent Multi-Strategy
Optimization (IMSO) to:
1. Maximize stiffness (k = F/δ)
2. Minimize mass (constraint: ≤ 0.2 kg)
Design Variables:
- support_angle: 30° - 90°
- tip_thickness: 15mm - 40mm
Usage:
python run_optimization.py [--trials N] [--dashboard]
"""
import sys
import json
import argparse
from pathlib import Path
from datetime import datetime
# Add project root to path
project_root = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(project_root))
import optuna
from optimization_engine.core.intelligent_optimizer import IntelligentOptimizer
from optimization_engine.nx.solver import NXSolver
from bracket_stiffness_extractor import BracketStiffnessExtractor
# Import central configuration
import config as atomizer_config
def load_config(config_file: Path) -> dict:
"""Load optimization configuration from JSON."""
with open(config_file, 'r') as f:
return json.load(f)
def create_objective_function(config: dict, study_dir: Path):
"""
Create the objective function for bracket optimization.
Returns a function that takes an Optuna trial and returns objectives.
"""
def objective(trial: optuna.Trial) -> tuple:
"""
Optimization objective function.
Args:
trial: Optuna trial object
Returns:
(stiffness_neg, mass): Tuple of objectives
- stiffness_neg: Negative stiffness (for minimization)
- mass: Mass in kg
"""
# Sample design variables
design_vars = {}
for dv in config['design_variables']:
design_vars[dv['name']] = trial.suggest_float(
dv['name'],
dv['min'],
dv['max']
)
print(f"\n{'='*60}")
print(f"Trial #{trial.number}")
print(f"{'='*60}")
print(f"Design Variables:")
for name, value in design_vars.items():
print(f" {name}: {value:.3f}")
# Model paths
model_dir = study_dir / "1_setup" / "model"
model_file = model_dir / "Bracket.prt"
sim_file = model_dir / "Bracket_sim1.sim"
# Initialize NX solver using central config
nx_solver = NXSolver(
nastran_version=atomizer_config.NX_VERSION,
timeout=atomizer_config.NASTRAN_TIMEOUT,
use_journal=True,
enable_session_management=True,
study_name="bracket_stiffness_optimization_V2"
)
# Run simulation with design variable updates
print(f"\nRunning simulation with updated design variables...")
try:
result = nx_solver.run_simulation(
sim_file=sim_file,
working_dir=model_dir,
expression_updates=design_vars,
solution_name=None # Solve all solutions in the .sim file
)
if not result['success']:
print(f"ERROR: Simulation failed: {result.get('errors', 'Unknown error')}")
raise optuna.exceptions.TrialPruned()
except Exception as e:
print(f"ERROR: Simulation failed: {e}")
raise optuna.exceptions.TrialPruned()
# Step 3: Extract results (stiffness and mass)
print(f"Extracting results...")
extractor = BracketStiffnessExtractor(model_dir=model_dir)
try:
results = extractor.extract_results()
except Exception as e:
print(f"ERROR: Extraction failed: {e}")
raise optuna.exceptions.TrialPruned()
stiffness = results['stiffness'] # N/mm
mass = results['mass'] # kg
mass_g = results['mass_g'] # grams
# Check constraint: mass ≤ 0.2 kg
mass_limit = config['constraints'][0]['value']
constraint_satisfied = mass <= mass_limit
# Store constraint status and all metrics
trial.set_user_attr("constraint_satisfied", constraint_satisfied)
trial.set_user_attr("mass_limit", mass_limit)
trial.set_user_attr("mass_violation", max(0, mass - mass_limit))
trial.set_user_attr("displacement", results['displacement'])
trial.set_user_attr("force", results['force'])
trial.set_user_attr("compliance", results['compliance'])
trial.set_user_attr("mass_g", mass_g)
if not constraint_satisfied:
print(f"\n[!] CONSTRAINT VIOLATED: Mass {mass:.6f} kg > {mass_limit} kg")
print(f" Trial will be kept for surrogate modeling but not eligible for Pareto front")
print(f"\n[OK] Trial Complete")
print(f" Stiffness: {stiffness:.2f} N/mm")
print(f" Mass: {mass:.6f} kg ({mass_g:.2f} g)")
print(f" Constraint: {'[OK] SATISFIED' if constraint_satisfied else '[X] VIOLATED'}")
# Return tuple of objectives
# Note: Optuna minimizes by default, so return negative for maximization
return -stiffness, mass # Maximize stiffness, minimize mass
return objective
def run_optimization(
config_file: Path,
study_dir: Path,
n_trials: int = None,
dashboard: bool = False
):
"""
Run bracket stiffness optimization using Protocol 10.
Args:
config_file: Path to optimization_config.json
study_dir: Path to study directory
n_trials: Number of trials (overrides config)
dashboard: Enable real-time dashboard
"""
# Load configuration
config = load_config(config_file)
study_name = config['study_name']
if n_trials is None:
n_trials = config['optimization_settings']['n_trials']
# Setup results directory
results_dir = study_dir / config['output_settings']['results_dir']
results_dir.mkdir(exist_ok=True)
# Extract design variable bounds
design_variables = {
dv['name']: (dv['min'], dv['max'])
for dv in config['design_variables']
}
print(f"\n{'='*60}")
print(f"BRACKET STIFFNESS OPTIMIZATION - PROTOCOL 10")
print(f"{'='*60}")
print(f"Study: {study_name}")
print(f"Trials: {n_trials}")
print(f"Objectives: Maximize stiffness, Minimize mass")
print(f"Constraint: Mass <= 0.2 kg (kept for surrogate, filtered from Pareto)")
print(f"Optimizer: Intelligent Multi-Strategy (Protocol 10)")
print(f"Results: {results_dir}")
print(f"{'='*60}\n")
# Create intelligent optimizer
intelligent_config = {
'intelligent_optimization': {
'enabled': True,
'min_analysis_trials': 10,
'stagnation_window': 10,
'min_improvement_threshold': 0.001,
'target_value': None, # No specific target for multi-objective
'enable_adaptive_surrogate': True,
'enable_strategy_switching': True
},
'optimization_settings': config['optimization_settings'],
'output_settings': config['output_settings']
}
optimizer = IntelligentOptimizer(
study_name=study_name,
study_dir=results_dir,
config=intelligent_config,
verbose=True
)
# Create objective function
objective_fn = create_objective_function(config, study_dir)
# Run optimization
start_time = datetime.now()
try:
results = optimizer.optimize(
objective_function=objective_fn,
design_variables=design_variables,
n_trials=n_trials,
target_value=None, # Multi-objective, no single target
directions=["minimize", "minimize"] # Minimize -stiffness (=maximize stiffness), minimize mass
)
except KeyboardInterrupt:
print("\n\nOptimization interrupted by user.")
results = None
end_time = datetime.now()
elapsed = (end_time - start_time).total_seconds()
# Print results summary
print(f"\n{'='*60}")
print(f"OPTIMIZATION COMPLETE")
print(f"{'='*60}")
if results:
print(f"\n[BEST] Best Solution Found:")
print(f" Stiffness: {-results['best_value'][0]:.2f} N/mm") # Convert back from negative
print(f" Mass: {results['best_value'][1]:.6f} kg")
print(f" Parameters: {results['best_params']}")
print(f"\n[STRATEGY] Strategy Performance:")
print(f" Final Strategy: {results.get('strategy_used', 'N/A')}")
if 'landscape_analysis' in results and results['landscape_analysis'] is not None:
print(f" Landscape Type: {results['landscape_analysis'].get('landscape_type', 'N/A')}")
# Access the Optuna study for detailed analysis
study = optimizer.study
if study:
completed_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]
pruned_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.PRUNED]
failed_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.FAIL]
# Count constraint violations
constraint_violated = [t for t in completed_trials if not t.user_attrs.get("constraint_satisfied", True)]
constraint_satisfied = [t for t in completed_trials if t.user_attrs.get("constraint_satisfied", True)]
print(f"\n[STATS] Trial Statistics:")
print(f" Total trials: {len(study.trials)}")
print(f" Completed: {len(completed_trials)}")
print(f" [OK] Feasible (constraint satisfied): {len(constraint_satisfied)}")
print(f" [X] Infeasible (constraint violated): {len(constraint_violated)}")
print(f" Pruned: {len(pruned_trials)}")
print(f" Failed: {len(failed_trials)}")
print(f" Elapsed time: {elapsed:.1f} seconds ({elapsed/60:.1f} minutes)")
# Get best trials (Pareto front) - filtered to only feasible solutions
try:
# Get all Pareto-optimal trials first
all_best_trials = study.best_trials
# Filter to only feasible solutions (constraint satisfied)
feasible_best_trials = [
t for t in all_best_trials
if t.user_attrs.get("constraint_satisfied", True)
]
print(f"\n[PARETO] Pareto Front Analysis:")
print(f" Pareto Front (all trials): {len(all_best_trials)} solutions")
print(f" Pareto Front (feasible only): {len(feasible_best_trials)} solutions")
if len(feasible_best_trials) > 0:
print(f"\n Top 5 Feasible Solutions:")
print(f" {'Trial':<8} {'Stiffness':<15} {'Mass':<15} {'Angle':<12} {'Thickness':<12}")
print(f" {'-' * 70}")
for i, trial in enumerate(feasible_best_trials[:5]):
stiff_neg, mass = trial.values
stiffness = -stiff_neg # Convert back to positive
angle = trial.params.get('support_angle', 0)
thickness = trial.params.get('tip_thickness', 0)
print(f" {trial.number:<8} {stiffness:<15.2f} {mass:<15.6f} {angle:<12.2f} {thickness:<12.2f}")
else:
print(f"\n [!] Warning: No feasible solutions found in Pareto front!")
except Exception as e:
print(f"\n Note: Could not retrieve Pareto front: {e}")
# Save summary
summary_file = results_dir / "optimization_summary.json"
summary = {
"study_name": study_name,
"optimizer": "Protocol 10 - Intelligent Multi-Strategy",
"n_trials": len(study.trials),
"completed_trials": len(completed_trials),
"feasible_trials": len(constraint_satisfied),
"infeasible_trials": len(constraint_violated),
"pruned_trials": len(pruned_trials),
"failed_trials": len(failed_trials),
"elapsed_seconds": elapsed,
"pareto_front_all": len(all_best_trials) if 'all_best_trials' in locals() else 0,
"pareto_front_feasible": len(feasible_best_trials) if 'feasible_best_trials' in locals() else 0,
"best_solution": results if results else None,
"timestamp": datetime.now().isoformat()
}
with open(summary_file, 'w') as f:
json.dump(summary, f, indent=2)
print(f"\n[SAVED] Summary saved to: {summary_file}")
print(f"\n{'='*60}\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run bracket stiffness optimization with Protocol 10")
parser.add_argument('--trials', type=int, default=None, help='Number of trials (default: from config)')
parser.add_argument('--dashboard', action='store_true', help='Enable real-time dashboard')
args = parser.parse_args()
# Paths
study_dir = Path(__file__).parent
config_file = study_dir / "optimization_config.json"
if not config_file.exists():
print(f"ERROR: Configuration file not found: {config_file}")
sys.exit(1)
# Run optimization
run_optimization(
config_file=config_file,
study_dir=study_dir,
n_trials=args.trials,
dashboard=args.dashboard
)