2025-11-26 12:01:50 -05:00
|
|
|
"""
|
|
|
|
|
Production-Ready NN Optimization with Confidence Bounds
|
|
|
|
|
|
|
|
|
|
This script runs multi-objective optimization using the CV-validated neural network
|
|
|
|
|
with proper extrapolation warnings and confidence-bounded results.
|
|
|
|
|
|
|
|
|
|
Key Features:
|
|
|
|
|
1. Uses CV-validated model with known accuracy (1.8% mass, 1.1% freq MAPE)
|
|
|
|
|
2. Warns when extrapolating outside training data range
|
|
|
|
|
3. Reads optimization bounds from study's optimization_config.json
|
|
|
|
|
4. Constrains optimization to prescribed bounds for reliable predictions
|
|
|
|
|
5. Marks designs needing FEA validation
|
|
|
|
|
"""
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import time
|
|
|
|
|
import json
|
|
|
|
|
import argparse
|
|
|
|
|
import torch
|
|
|
|
|
import numpy as np
|
|
|
|
|
import optuna
|
|
|
|
|
from optuna.samplers import NSGAIISampler
|
|
|
|
|
import matplotlib
|
|
|
|
|
matplotlib.use('Agg')
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
|
|
|
|
# Add project paths
|
|
|
|
|
project_root = Path(__file__).parent
|
|
|
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_config_bounds(study_path: Path) -> dict:
|
|
|
|
|
"""Load design variable bounds from optimization_config.json
|
|
|
|
|
|
|
|
|
|
Returns dict: {param_name: (min, max, is_int)}
|
|
|
|
|
"""
|
|
|
|
|
config_path = study_path / "1_setup" / "optimization_config.json"
|
|
|
|
|
|
|
|
|
|
if not config_path.exists():
|
|
|
|
|
raise FileNotFoundError(f"Config not found: {config_path}")
|
|
|
|
|
|
|
|
|
|
with open(config_path) as f:
|
|
|
|
|
config = json.load(f)
|
|
|
|
|
|
|
|
|
|
bounds = {}
|
|
|
|
|
for var in config.get('design_variables', []):
|
|
|
|
|
# Support both 'parameter' and 'name' keys
|
|
|
|
|
name = var.get('parameter') or var.get('name')
|
|
|
|
|
|
|
|
|
|
# Support both "bounds": [min, max] and "min_value"/"max_value" formats
|
|
|
|
|
if 'bounds' in var:
|
|
|
|
|
min_val, max_val = var['bounds']
|
|
|
|
|
else:
|
|
|
|
|
min_val = var.get('min_value', var.get('min', 0))
|
|
|
|
|
max_val = var.get('max_value', var.get('max', 1))
|
|
|
|
|
|
|
|
|
|
# Detect integer type based on name or explicit type
|
|
|
|
|
is_int = (var.get('type') == 'integer' or
|
|
|
|
|
'count' in name.lower() or
|
|
|
|
|
(isinstance(min_val, int) and isinstance(max_val, int) and max_val - min_val < 20))
|
|
|
|
|
|
|
|
|
|
bounds[name] = (min_val, max_val, is_int)
|
|
|
|
|
|
|
|
|
|
return bounds
|
|
|
|
|
|
refactor: Major reorganization of optimization_engine module structure
BREAKING CHANGE: Module paths have been reorganized for better maintainability.
Backwards compatibility aliases with deprecation warnings are provided.
New Structure:
- core/ - Optimization runners (runner, intelligent_optimizer, etc.)
- processors/ - Data processing
- surrogates/ - Neural network surrogates
- nx/ - NX/Nastran integration (solver, updater, session_manager)
- study/ - Study management (creator, wizard, state, reset)
- reporting/ - Reports and analysis (visualizer, report_generator)
- config/ - Configuration management (manager, builder)
- utils/ - Utilities (logger, auto_doc, etc.)
- future/ - Research/experimental code
Migration:
- ~200 import changes across 125 files
- All __init__.py files use lazy loading to avoid circular imports
- Backwards compatibility layer supports old import paths with warnings
- All existing functionality preserved
To migrate existing code:
OLD: from optimization_engine.nx_solver import NXSolver
NEW: from optimization_engine.nx.solver import NXSolver
OLD: from optimization_engine.runner import OptimizationRunner
NEW: from optimization_engine.core.runner import OptimizationRunner
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 12:30:59 -05:00
|
|
|
from optimization_engine.processors.surrogates.active_learning_surrogate import EnsembleMLP
|
2025-11-26 12:01:50 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ValidatedSurrogate:
|
|
|
|
|
"""Surrogate with CV-validated accuracy and extrapolation detection."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, model_path: str):
|
|
|
|
|
state = torch.load(model_path, map_location='cpu')
|
|
|
|
|
|
|
|
|
|
self.model = EnsembleMLP(
|
|
|
|
|
input_dim=len(state['design_var_names']),
|
|
|
|
|
output_dim=4, # mass, freq, disp, stress
|
|
|
|
|
hidden_dims=state['hidden_dims']
|
|
|
|
|
)
|
|
|
|
|
self.model.load_state_dict(state['model'])
|
|
|
|
|
self.model.eval()
|
|
|
|
|
|
|
|
|
|
self.input_mean = np.array(state['input_mean'])
|
|
|
|
|
self.input_std = np.array(state['input_std'])
|
|
|
|
|
self.output_mean = np.array(state['output_mean'])
|
|
|
|
|
self.output_std = np.array(state['output_std'])
|
|
|
|
|
self.design_var_names = state['design_var_names']
|
|
|
|
|
|
|
|
|
|
# CV metrics
|
|
|
|
|
self.cv_mass_mape = state['cv_mass_mape']
|
|
|
|
|
self.cv_freq_mape = state['cv_freq_mape']
|
|
|
|
|
self.cv_mass_std = state['cv_mass_std']
|
|
|
|
|
self.cv_freq_std = state['cv_freq_std']
|
|
|
|
|
self.n_training_samples = state['n_samples']
|
|
|
|
|
|
|
|
|
|
# Training bounds (for extrapolation detection)
|
|
|
|
|
self.bounds_min = self.input_mean - 2 * self.input_std
|
|
|
|
|
self.bounds_max = self.input_mean + 2 * self.input_std
|
|
|
|
|
|
|
|
|
|
def predict(self, params: dict) -> dict:
|
|
|
|
|
"""Predict with extrapolation check."""
|
|
|
|
|
x = np.array([[params.get(name, 0.0) for name in self.design_var_names]], dtype=np.float32)
|
|
|
|
|
|
|
|
|
|
# Check for extrapolation
|
|
|
|
|
extrapolation_score = 0.0
|
|
|
|
|
for i, name in enumerate(self.design_var_names):
|
|
|
|
|
val = x[0, i]
|
|
|
|
|
if val < self.bounds_min[i]:
|
|
|
|
|
extrapolation_score += (self.bounds_min[i] - val) / (self.input_std[i] + 1e-8)
|
|
|
|
|
elif val > self.bounds_max[i]:
|
|
|
|
|
extrapolation_score += (val - self.bounds_max[i]) / (self.input_std[i] + 1e-8)
|
|
|
|
|
|
|
|
|
|
# Normalize input
|
|
|
|
|
x_norm = (x - self.input_mean) / (self.input_std + 1e-8)
|
|
|
|
|
x_t = torch.FloatTensor(x_norm)
|
|
|
|
|
|
|
|
|
|
# Predict
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
pred_norm = self.model(x_t).numpy()
|
|
|
|
|
|
|
|
|
|
pred = pred_norm * (self.output_std + 1e-8) + self.output_mean
|
|
|
|
|
|
|
|
|
|
# Calculate confidence-adjusted uncertainty
|
|
|
|
|
base_uncertainty = self.cv_mass_mape / 100 # Base uncertainty from CV
|
|
|
|
|
extrapolation_penalty = min(extrapolation_score * 0.1, 0.5) # Max 50% extra uncertainty
|
|
|
|
|
total_uncertainty = base_uncertainty + extrapolation_penalty
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'mass': float(pred[0, 0]),
|
|
|
|
|
'frequency': float(pred[0, 1]),
|
|
|
|
|
'max_displacement': float(pred[0, 2]),
|
|
|
|
|
'max_stress': float(pred[0, 3]),
|
|
|
|
|
'uncertainty': total_uncertainty,
|
|
|
|
|
'extrapolating': extrapolation_score > 0.1,
|
|
|
|
|
'extrapolation_score': extrapolation_score,
|
|
|
|
|
'needs_fea_validation': extrapolation_score > 0.5
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_optimization(surrogate: ValidatedSurrogate, bounds: dict, n_trials: int = 1000):
|
|
|
|
|
"""Run multi-objective optimization.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
surrogate: ValidatedSurrogate model
|
|
|
|
|
bounds: Dict from load_config_bounds {name: (min, max, is_int)}
|
|
|
|
|
n_trials: Number of optimization trials
|
|
|
|
|
"""
|
|
|
|
|
print(f"\nOptimization bounds (from config):")
|
|
|
|
|
for name, (low, high, is_int) in bounds.items():
|
|
|
|
|
type_str = "int" if is_int else "float"
|
|
|
|
|
print(f" {name}: [{low}, {high}] ({type_str})")
|
|
|
|
|
|
|
|
|
|
# Create study
|
|
|
|
|
study = optuna.create_study(
|
|
|
|
|
directions=["minimize", "minimize"], # mass, -frequency
|
|
|
|
|
sampler=NSGAIISampler()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Track stats
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
trial_times = []
|
|
|
|
|
extrapolation_count = 0
|
|
|
|
|
|
|
|
|
|
def objective(trial: optuna.Trial):
|
|
|
|
|
nonlocal extrapolation_count
|
|
|
|
|
|
|
|
|
|
params = {}
|
|
|
|
|
for name, (low, high, is_int) in bounds.items():
|
|
|
|
|
if is_int:
|
|
|
|
|
params[name] = trial.suggest_int(name, int(low), int(high))
|
|
|
|
|
else:
|
|
|
|
|
params[name] = trial.suggest_float(name, float(low), float(high))
|
|
|
|
|
|
|
|
|
|
trial_start = time.time()
|
|
|
|
|
result = surrogate.predict(params)
|
|
|
|
|
trial_time = (time.time() - trial_start) * 1000
|
|
|
|
|
trial_times.append(trial_time)
|
|
|
|
|
|
|
|
|
|
# Track extrapolation
|
|
|
|
|
if result['extrapolating']:
|
|
|
|
|
extrapolation_count += 1
|
|
|
|
|
|
|
|
|
|
# Store metadata
|
|
|
|
|
trial.set_user_attr('uncertainty', result['uncertainty'])
|
|
|
|
|
trial.set_user_attr('extrapolating', result['extrapolating'])
|
|
|
|
|
trial.set_user_attr('needs_fea', result['needs_fea_validation'])
|
|
|
|
|
trial.set_user_attr('params', params)
|
|
|
|
|
|
|
|
|
|
if trial.number % 200 == 0:
|
|
|
|
|
print(f" Trial {trial.number}: mass={result['mass']:.1f}g, freq={result['frequency']:.2f}Hz, extrap={result['extrapolating']}")
|
|
|
|
|
|
|
|
|
|
return result['mass'], -result['frequency']
|
|
|
|
|
|
|
|
|
|
print(f"\nRunning {n_trials} trials...")
|
|
|
|
|
study.optimize(objective, n_trials=n_trials, show_progress_bar=True)
|
|
|
|
|
|
|
|
|
|
total_time = time.time() - start_time
|
|
|
|
|
|
|
|
|
|
return study, {
|
|
|
|
|
'total_time': total_time,
|
|
|
|
|
'avg_trial_time_ms': np.mean(trial_times),
|
|
|
|
|
'trials_per_second': n_trials / total_time,
|
|
|
|
|
'extrapolation_count': extrapolation_count,
|
|
|
|
|
'extrapolation_pct': extrapolation_count / n_trials * 100
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def analyze_pareto_front(study, surrogate):
|
|
|
|
|
"""Analyze Pareto front with confidence information."""
|
|
|
|
|
pareto_trials = study.best_trials
|
|
|
|
|
|
|
|
|
|
results = {
|
|
|
|
|
'total_pareto_designs': len(pareto_trials),
|
|
|
|
|
'confident_designs': 0,
|
|
|
|
|
'needs_fea_designs': 0,
|
|
|
|
|
'designs': []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for trial in pareto_trials:
|
|
|
|
|
mass = trial.values[0]
|
|
|
|
|
freq = -trial.values[1]
|
|
|
|
|
uncertainty = trial.user_attrs.get('uncertainty', 0)
|
|
|
|
|
needs_fea = trial.user_attrs.get('needs_fea', False)
|
|
|
|
|
params = trial.user_attrs.get('params', trial.params)
|
|
|
|
|
|
|
|
|
|
if needs_fea:
|
|
|
|
|
results['needs_fea_designs'] += 1
|
|
|
|
|
else:
|
|
|
|
|
results['confident_designs'] += 1
|
|
|
|
|
|
|
|
|
|
results['designs'].append({
|
|
|
|
|
'mass': mass,
|
|
|
|
|
'frequency': freq,
|
|
|
|
|
'uncertainty': uncertainty,
|
|
|
|
|
'needs_fea': needs_fea,
|
|
|
|
|
'params': params
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Sort by mass
|
|
|
|
|
results['designs'].sort(key=lambda x: x['mass'])
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def plot_results(study, pareto_analysis, surrogate, save_path):
|
|
|
|
|
"""Generate visualization of optimization results."""
|
|
|
|
|
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
|
|
|
|
|
|
|
|
|
|
# 1. Pareto Front with Confidence
|
|
|
|
|
ax = axes[0, 0]
|
|
|
|
|
pareto = pareto_analysis['designs']
|
|
|
|
|
|
|
|
|
|
confident = [d for d in pareto if not d['needs_fea']]
|
|
|
|
|
needs_fea = [d for d in pareto if d['needs_fea']]
|
|
|
|
|
|
|
|
|
|
if confident:
|
|
|
|
|
ax.scatter([d['mass'] for d in confident],
|
|
|
|
|
[d['frequency'] for d in confident],
|
|
|
|
|
c='green', s=50, alpha=0.7, label=f'Confident ({len(confident)})')
|
|
|
|
|
|
|
|
|
|
if needs_fea:
|
|
|
|
|
ax.scatter([d['mass'] for d in needs_fea],
|
|
|
|
|
[d['frequency'] for d in needs_fea],
|
|
|
|
|
c='orange', s=50, alpha=0.7, marker='^', label=f'Needs FEA ({len(needs_fea)})')
|
|
|
|
|
|
|
|
|
|
ax.set_xlabel('Mass (g)')
|
|
|
|
|
ax.set_ylabel('Frequency (Hz)')
|
|
|
|
|
ax.set_title('Pareto Front with Confidence Assessment')
|
|
|
|
|
ax.legend()
|
|
|
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
|
|
|
|
|
|
# 2. Uncertainty vs Performance
|
|
|
|
|
ax = axes[0, 1]
|
|
|
|
|
all_trials = study.trials
|
|
|
|
|
masses = [t.values[0] for t in all_trials if t.values]
|
|
|
|
|
freqs = [-t.values[1] for t in all_trials if t.values]
|
|
|
|
|
uncertainties = [t.user_attrs.get('uncertainty', 0) for t in all_trials if t.values]
|
|
|
|
|
|
|
|
|
|
sc = ax.scatter(masses, freqs, c=uncertainties, cmap='RdYlGn_r', s=10, alpha=0.5)
|
|
|
|
|
plt.colorbar(sc, ax=ax, label='Uncertainty')
|
|
|
|
|
ax.set_xlabel('Mass (g)')
|
|
|
|
|
ax.set_ylabel('Frequency (Hz)')
|
|
|
|
|
ax.set_title('All Trials Colored by Uncertainty')
|
|
|
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
|
|
|
|
|
|
# 3. Top 10 Pareto Designs
|
|
|
|
|
ax = axes[1, 0]
|
|
|
|
|
top_10 = pareto_analysis['designs'][:10]
|
|
|
|
|
|
|
|
|
|
y_pos = np.arange(len(top_10))
|
|
|
|
|
bars = ax.barh(y_pos, [d['mass'] for d in top_10],
|
|
|
|
|
color=['green' if not d['needs_fea'] else 'orange' for d in top_10])
|
|
|
|
|
|
|
|
|
|
ax.set_yticks(y_pos)
|
|
|
|
|
ax.set_yticklabels([f"#{i+1}: {d['frequency']:.1f}Hz" for i, d in enumerate(top_10)])
|
|
|
|
|
ax.set_xlabel('Mass (g)')
|
|
|
|
|
ax.set_title('Top 10 Lowest Mass Designs')
|
|
|
|
|
ax.grid(True, alpha=0.3, axis='x')
|
|
|
|
|
|
|
|
|
|
# Add confidence text
|
|
|
|
|
for i, (bar, d) in enumerate(zip(bars, top_10)):
|
|
|
|
|
status = "FEA!" if d['needs_fea'] else "OK"
|
|
|
|
|
ax.text(bar.get_width() + 10, bar.get_y() + bar.get_height()/2,
|
|
|
|
|
status, va='center', fontsize=9)
|
|
|
|
|
|
|
|
|
|
# 4. Summary Text
|
|
|
|
|
ax = axes[1, 1]
|
|
|
|
|
ax.axis('off')
|
|
|
|
|
|
|
|
|
|
summary_text = f"""
|
|
|
|
|
OPTIMIZATION SUMMARY
|
|
|
|
|
====================
|
|
|
|
|
|
|
|
|
|
CV-Validated Model Accuracy:
|
|
|
|
|
Mass MAPE: {surrogate.cv_mass_mape:.1f}% +/- {surrogate.cv_mass_std:.1f}%
|
|
|
|
|
Freq MAPE: {surrogate.cv_freq_mape:.1f}% +/- {surrogate.cv_freq_std:.1f}%
|
|
|
|
|
Training samples: {surrogate.n_training_samples}
|
|
|
|
|
|
|
|
|
|
Pareto Front:
|
|
|
|
|
Total designs: {pareto_analysis['total_pareto_designs']}
|
|
|
|
|
High confidence: {pareto_analysis['confident_designs']}
|
|
|
|
|
Needs FEA validation: {pareto_analysis['needs_fea_designs']}
|
|
|
|
|
|
|
|
|
|
Best Confident Design:
|
|
|
|
|
"""
|
|
|
|
|
# Find best confident design
|
|
|
|
|
best_confident = [d for d in pareto_analysis['designs'] if not d['needs_fea']]
|
|
|
|
|
if best_confident:
|
|
|
|
|
best = best_confident[0]
|
|
|
|
|
summary_text += f"""
|
|
|
|
|
Mass: {best['mass']:.1f}g
|
|
|
|
|
Frequency: {best['frequency']:.2f}Hz
|
|
|
|
|
Parameters:
|
|
|
|
|
"""
|
|
|
|
|
for k, v in best['params'].items():
|
|
|
|
|
if isinstance(v, float):
|
|
|
|
|
summary_text += f" {k}: {v:.3f}\n"
|
|
|
|
|
else:
|
|
|
|
|
summary_text += f" {k}: {v}\n"
|
|
|
|
|
else:
|
|
|
|
|
summary_text += " No confident designs found - run more FEA!"
|
|
|
|
|
|
|
|
|
|
ax.text(0.05, 0.95, summary_text, transform=ax.transAxes,
|
|
|
|
|
fontfamily='monospace', fontsize=10, verticalalignment='top')
|
|
|
|
|
|
|
|
|
|
plt.suptitle('NN-Based Multi-Objective Optimization Results', fontsize=14, fontweight='bold')
|
|
|
|
|
plt.tight_layout()
|
|
|
|
|
plt.savefig(save_path, dpi=150)
|
|
|
|
|
plt.close()
|
|
|
|
|
print(f"Saved plot: {save_path}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
parser = argparse.ArgumentParser(description='NN Optimization with Confidence Bounds')
|
|
|
|
|
parser.add_argument('--study', default='uav_arm_optimization',
|
|
|
|
|
help='Study name (e.g., uav_arm_optimization)')
|
|
|
|
|
parser.add_argument('--model', default=None,
|
|
|
|
|
help='Path to CV-validated model (default: cv_validated_surrogate.pt)')
|
|
|
|
|
parser.add_argument('--trials', type=int, default=2000,
|
|
|
|
|
help='Number of optimization trials')
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
print("="*70)
|
|
|
|
|
print("Production-Ready NN Optimization with Confidence Bounds")
|
|
|
|
|
print("="*70)
|
|
|
|
|
|
|
|
|
|
# Load bounds from study config
|
|
|
|
|
study_path = project_root / "studies" / args.study
|
|
|
|
|
if not study_path.exists():
|
|
|
|
|
print(f"ERROR: Study not found: {study_path}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"\nLoading bounds from study: {args.study}")
|
|
|
|
|
bounds = load_config_bounds(study_path)
|
|
|
|
|
print(f" Loaded {len(bounds)} design variables from config")
|
|
|
|
|
|
|
|
|
|
# Load CV-validated model
|
|
|
|
|
model_path = Path(args.model) if args.model else project_root / "cv_validated_surrogate.pt"
|
|
|
|
|
if not model_path.exists():
|
|
|
|
|
print(f"ERROR: Run validate_surrogate_real_data.py first to create {model_path}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"\nLoading CV-validated model: {model_path}")
|
|
|
|
|
surrogate = ValidatedSurrogate(str(model_path))
|
|
|
|
|
|
|
|
|
|
print(f"\nModel Statistics:")
|
|
|
|
|
print(f" Training samples: {surrogate.n_training_samples}")
|
|
|
|
|
print(f" CV Mass MAPE: {surrogate.cv_mass_mape:.1f}% +/- {surrogate.cv_mass_std:.1f}%")
|
|
|
|
|
print(f" CV Freq MAPE: {surrogate.cv_freq_mape:.1f}% +/- {surrogate.cv_freq_std:.1f}%")
|
|
|
|
|
print(f" Design variables: {surrogate.design_var_names}")
|
|
|
|
|
|
|
|
|
|
# Verify model variables match config variables
|
|
|
|
|
config_vars = set(bounds.keys())
|
|
|
|
|
model_vars = set(surrogate.design_var_names)
|
|
|
|
|
if config_vars != model_vars:
|
|
|
|
|
print(f"\nWARNING: Variable mismatch!")
|
|
|
|
|
print(f" Config has: {config_vars}")
|
|
|
|
|
print(f" Model has: {model_vars}")
|
|
|
|
|
print(f" Missing from model: {config_vars - model_vars}")
|
|
|
|
|
print(f" Extra in model: {model_vars - config_vars}")
|
|
|
|
|
|
|
|
|
|
# Run optimization
|
|
|
|
|
print("\n" + "="*70)
|
|
|
|
|
print("Running Multi-Objective Optimization")
|
|
|
|
|
print("="*70)
|
|
|
|
|
|
|
|
|
|
study, stats = run_optimization(surrogate, bounds, n_trials=args.trials)
|
|
|
|
|
|
|
|
|
|
print(f"\nOptimization Statistics:")
|
|
|
|
|
print(f" Total time: {stats['total_time']:.1f}s")
|
|
|
|
|
print(f" Avg trial time: {stats['avg_trial_time_ms']:.2f}ms")
|
|
|
|
|
print(f" Trials per second: {stats['trials_per_second']:.1f}")
|
|
|
|
|
print(f" Extrapolating trials: {stats['extrapolation_count']} ({stats['extrapolation_pct']:.1f}%)")
|
|
|
|
|
|
|
|
|
|
# Analyze Pareto front
|
|
|
|
|
print("\n" + "="*70)
|
|
|
|
|
print("Pareto Front Analysis")
|
|
|
|
|
print("="*70)
|
|
|
|
|
|
|
|
|
|
pareto_analysis = analyze_pareto_front(study, surrogate)
|
|
|
|
|
|
|
|
|
|
print(f"\nPareto Front Summary:")
|
|
|
|
|
print(f" Total Pareto-optimal designs: {pareto_analysis['total_pareto_designs']}")
|
|
|
|
|
print(f" High confidence designs: {pareto_analysis['confident_designs']}")
|
|
|
|
|
print(f" Needs FEA validation: {pareto_analysis['needs_fea_designs']}")
|
|
|
|
|
|
|
|
|
|
print(f"\nTop 5 Lowest Mass Designs:")
|
|
|
|
|
for i, d in enumerate(pareto_analysis['designs'][:5]):
|
|
|
|
|
status = "[NEEDS FEA]" if d['needs_fea'] else "[OK]"
|
|
|
|
|
print(f" {i+1}. Mass={d['mass']:.1f}g, Freq={d['frequency']:.2f}Hz {status}")
|
|
|
|
|
print(f" Params: {d['params']}")
|
|
|
|
|
|
|
|
|
|
# Generate plots
|
|
|
|
|
plot_path = project_root / "validated_nn_optimization_results.png"
|
|
|
|
|
plot_results(study, pareto_analysis, surrogate, str(plot_path))
|
|
|
|
|
|
|
|
|
|
# Save results
|
|
|
|
|
results_path = project_root / "validated_nn_optimization_results.json"
|
|
|
|
|
with open(results_path, 'w') as f:
|
|
|
|
|
json.dump({
|
|
|
|
|
'stats': stats,
|
|
|
|
|
'pareto_summary': {
|
|
|
|
|
'total': pareto_analysis['total_pareto_designs'],
|
|
|
|
|
'confident': pareto_analysis['confident_designs'],
|
|
|
|
|
'needs_fea': pareto_analysis['needs_fea_designs']
|
|
|
|
|
},
|
|
|
|
|
'top_designs': pareto_analysis['designs'][:20],
|
|
|
|
|
'cv_metrics': {
|
|
|
|
|
'mass_mape': surrogate.cv_mass_mape,
|
|
|
|
|
'freq_mape': surrogate.cv_freq_mape
|
|
|
|
|
}
|
|
|
|
|
}, f, indent=2)
|
|
|
|
|
print(f"\nSaved results: {results_path}")
|
|
|
|
|
|
|
|
|
|
print("\n" + "="*70)
|
|
|
|
|
print("NEXT STEPS")
|
|
|
|
|
print("="*70)
|
|
|
|
|
|
|
|
|
|
if pareto_analysis['confident_designs'] > 0:
|
|
|
|
|
print("1. Review the confident Pareto-optimal designs above")
|
|
|
|
|
print("2. Consider validating top 3-5 designs with actual FEA")
|
|
|
|
|
print("3. If FEA matches predictions, use for manufacturing")
|
|
|
|
|
else:
|
|
|
|
|
print("1. All Pareto designs are extrapolating - need more FEA data!")
|
|
|
|
|
print("2. Run FEA on designs marked [NEEDS FEA]")
|
|
|
|
|
print("3. Retrain the model with new data")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
main()
|