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>
404 lines
14 KiB
Python
404 lines
14 KiB
Python
"""
|
|
Optimization Configuration Builder
|
|
|
|
Helps users build multi-objective optimization configurations by:
|
|
1. Discovering available design variables from FEA model
|
|
2. Listing available objectives and constraints
|
|
3. Creating structured optimization_config.json
|
|
|
|
Supports:
|
|
- Multi-objective optimization (minimize weight + stress simultaneously)
|
|
- Constraints (max displacement, stress limits, mass limits)
|
|
- User selection of which objectives/constraints to apply
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, Any, List
|
|
import json
|
|
|
|
|
|
class OptimizationConfigBuilder:
|
|
"""
|
|
Interactive builder for optimization configurations.
|
|
|
|
Workflow:
|
|
1. Discover model capabilities (design variables, analysis type)
|
|
2. Present available objectives/constraints to user
|
|
3. Build configuration based on user selections
|
|
"""
|
|
|
|
# Available objectives that can be extracted from OP2 files
|
|
AVAILABLE_OBJECTIVES = {
|
|
'minimize_mass': {
|
|
'description': 'Minimize total mass (weight reduction)',
|
|
'extractor': 'mass_extractor',
|
|
'metric': 'total_mass',
|
|
'units': 'kg',
|
|
'direction': 'minimize',
|
|
'typical_weight': 5.0 # Higher priority in multi-objective
|
|
},
|
|
'minimize_max_stress': {
|
|
'description': 'Minimize maximum von Mises stress',
|
|
'extractor': 'stress_extractor',
|
|
'metric': 'max_von_mises',
|
|
'units': 'MPa',
|
|
'direction': 'minimize',
|
|
'typical_weight': 10.0 # Very important - failure prevention
|
|
},
|
|
'minimize_max_displacement': {
|
|
'description': 'Minimize maximum displacement (increase stiffness)',
|
|
'extractor': 'displacement_extractor',
|
|
'metric': 'max_displacement',
|
|
'units': 'mm',
|
|
'direction': 'minimize',
|
|
'typical_weight': 3.0
|
|
},
|
|
'minimize_volume': {
|
|
'description': 'Minimize total volume (material usage)',
|
|
'extractor': 'volume_extractor',
|
|
'metric': 'total_volume',
|
|
'units': 'mm^3',
|
|
'direction': 'minimize',
|
|
'typical_weight': 4.0
|
|
}
|
|
}
|
|
|
|
# Available constraints
|
|
AVAILABLE_CONSTRAINTS = {
|
|
'max_stress_limit': {
|
|
'description': 'Maximum allowable von Mises stress',
|
|
'extractor': 'stress_extractor',
|
|
'metric': 'max_von_mises',
|
|
'units': 'MPa',
|
|
'typical_value': 200.0, # Below yield strength with safety factor
|
|
'constraint_type': 'upper_bound'
|
|
},
|
|
'max_displacement_limit': {
|
|
'description': 'Maximum allowable displacement',
|
|
'extractor': 'displacement_extractor',
|
|
'metric': 'max_displacement',
|
|
'units': 'mm',
|
|
'typical_value': 1.0, # Stiffness requirement
|
|
'constraint_type': 'upper_bound'
|
|
},
|
|
'min_mass_limit': {
|
|
'description': 'Minimum required mass (structural integrity)',
|
|
'extractor': 'mass_extractor',
|
|
'metric': 'total_mass',
|
|
'units': 'kg',
|
|
'typical_value': 0.3,
|
|
'constraint_type': 'lower_bound'
|
|
},
|
|
'max_mass_limit': {
|
|
'description': 'Maximum allowable mass (weight budget)',
|
|
'extractor': 'mass_extractor',
|
|
'metric': 'total_mass',
|
|
'units': 'kg',
|
|
'typical_value': 0.5,
|
|
'constraint_type': 'upper_bound'
|
|
}
|
|
}
|
|
|
|
def __init__(self, model_discovery_result: Dict[str, Any]):
|
|
"""
|
|
Initialize with model discovery results.
|
|
|
|
Args:
|
|
model_discovery_result: Output from discover_fea_model()
|
|
"""
|
|
self.model_info = model_discovery_result
|
|
self.config = {
|
|
'design_variables': [],
|
|
'objectives': [],
|
|
'constraints': [],
|
|
'optimization_settings': {
|
|
'n_trials': 100,
|
|
'sampler': 'TPE',
|
|
'n_startup_trials': 20
|
|
}
|
|
}
|
|
|
|
def list_available_design_variables(self) -> List[Dict[str, Any]]:
|
|
"""
|
|
List all available design variables from model.
|
|
|
|
Returns:
|
|
List of design variable options
|
|
"""
|
|
if 'expressions' not in self.model_info:
|
|
return []
|
|
|
|
design_vars = []
|
|
for expr in self.model_info['expressions']:
|
|
if expr['value'] is not None: # Only variables with known values
|
|
design_vars.append({
|
|
'name': expr['name'],
|
|
'current_value': expr['value'],
|
|
'units': expr['units'],
|
|
'type': expr.get('type', 'Unknown'),
|
|
'suggested_bounds': self._suggest_bounds(expr)
|
|
})
|
|
|
|
return design_vars
|
|
|
|
def _suggest_bounds(self, expr: Dict[str, Any]) -> tuple:
|
|
"""
|
|
Suggest reasonable optimization bounds for a design variable.
|
|
|
|
Args:
|
|
expr: Expression dictionary
|
|
|
|
Returns:
|
|
(lower_bound, upper_bound)
|
|
"""
|
|
value = expr['value']
|
|
expr_type = expr.get('type', '').lower()
|
|
|
|
if 'angle' in expr_type or 'degrees' in expr.get('units', '').lower():
|
|
# Angles: ±15 degrees
|
|
return (max(0, value - 15), min(180, value + 15))
|
|
elif 'thickness' in expr['name'].lower() or 'dimension' in expr_type:
|
|
# Dimensions: ±30%
|
|
return (value * 0.7, value * 1.3)
|
|
elif 'radius' in expr['name'].lower() or 'diameter' in expr['name'].lower():
|
|
# Radii/diameters: ±25%
|
|
return (value * 0.75, value * 1.25)
|
|
else:
|
|
# Default: ±20%
|
|
return (value * 0.8, value * 1.2)
|
|
|
|
def list_available_objectives(self) -> Dict[str, Dict[str, Any]]:
|
|
"""
|
|
List all available optimization objectives.
|
|
|
|
Returns:
|
|
Dictionary of objective options
|
|
"""
|
|
return self.AVAILABLE_OBJECTIVES.copy()
|
|
|
|
def list_available_constraints(self) -> Dict[str, Dict[str, Any]]:
|
|
"""
|
|
List all available constraints.
|
|
|
|
Returns:
|
|
Dictionary of constraint options
|
|
"""
|
|
return self.AVAILABLE_CONSTRAINTS.copy()
|
|
|
|
def add_design_variable(self, name: str, lower_bound: float, upper_bound: float):
|
|
"""
|
|
Add a design variable to the configuration.
|
|
|
|
Args:
|
|
name: Expression name from model
|
|
lower_bound: Minimum value
|
|
upper_bound: Maximum value
|
|
"""
|
|
# Verify variable exists in model
|
|
expr = next((e for e in self.model_info['expressions'] if e['name'] == name), None)
|
|
if not expr:
|
|
raise ValueError(f"Design variable '{name}' not found in model")
|
|
|
|
self.config['design_variables'].append({
|
|
'name': name,
|
|
'type': 'continuous',
|
|
'bounds': [lower_bound, upper_bound],
|
|
'units': expr.get('units', ''),
|
|
'initial_value': expr['value']
|
|
})
|
|
|
|
def add_objective(self, objective_key: str, weight: float = None, target: float = None):
|
|
"""
|
|
Add an objective to the configuration.
|
|
|
|
Args:
|
|
objective_key: Key from AVAILABLE_OBJECTIVES
|
|
weight: Importance weight (for multi-objective)
|
|
target: Target value (optional, for goal programming)
|
|
"""
|
|
if objective_key not in self.AVAILABLE_OBJECTIVES:
|
|
raise ValueError(f"Unknown objective: {objective_key}")
|
|
|
|
obj_info = self.AVAILABLE_OBJECTIVES[objective_key]
|
|
|
|
objective = {
|
|
'name': objective_key,
|
|
'description': obj_info['description'],
|
|
'extractor': obj_info['extractor'],
|
|
'metric': obj_info['metric'],
|
|
'direction': obj_info['direction'],
|
|
'weight': weight or obj_info['typical_weight']
|
|
}
|
|
|
|
if target is not None:
|
|
objective['target'] = target
|
|
|
|
self.config['objectives'].append(objective)
|
|
|
|
def add_constraint(self, constraint_key: str, limit_value: float):
|
|
"""
|
|
Add a constraint to the configuration.
|
|
|
|
Args:
|
|
constraint_key: Key from AVAILABLE_CONSTRAINTS
|
|
limit_value: Constraint limit value
|
|
"""
|
|
if constraint_key not in self.AVAILABLE_CONSTRAINTS:
|
|
raise ValueError(f"Unknown constraint: {constraint_key}")
|
|
|
|
const_info = self.AVAILABLE_CONSTRAINTS[constraint_key]
|
|
|
|
constraint = {
|
|
'name': constraint_key,
|
|
'description': const_info['description'],
|
|
'extractor': const_info['extractor'],
|
|
'metric': const_info['metric'],
|
|
'type': const_info['constraint_type'],
|
|
'limit': limit_value,
|
|
'units': const_info['units']
|
|
}
|
|
|
|
self.config['constraints'].append(constraint)
|
|
|
|
def set_optimization_settings(self, n_trials: int = None, sampler: str = None):
|
|
"""
|
|
Configure optimization algorithm settings.
|
|
|
|
Args:
|
|
n_trials: Number of optimization iterations
|
|
sampler: 'TPE', 'CMAES', 'GP', etc.
|
|
"""
|
|
if n_trials:
|
|
self.config['optimization_settings']['n_trials'] = n_trials
|
|
if sampler:
|
|
self.config['optimization_settings']['sampler'] = sampler
|
|
|
|
def build(self) -> Dict[str, Any]:
|
|
"""
|
|
Build and validate the configuration.
|
|
|
|
Returns:
|
|
Complete optimization configuration
|
|
"""
|
|
# Validation
|
|
if not self.config['design_variables']:
|
|
raise ValueError("At least one design variable is required")
|
|
|
|
if not self.config['objectives']:
|
|
raise ValueError("At least one objective is required")
|
|
|
|
# Add metadata
|
|
self.config['model_info'] = {
|
|
'sim_file': self.model_info.get('sim_file', ''),
|
|
'solutions': self.model_info.get('solutions', [])
|
|
}
|
|
|
|
return self.config
|
|
|
|
def save(self, output_path: Path):
|
|
"""
|
|
Save configuration to JSON file.
|
|
|
|
Args:
|
|
output_path: Path to save configuration
|
|
"""
|
|
config = self.build()
|
|
|
|
with open(output_path, 'w') as f:
|
|
json.dump(config, f, indent=2)
|
|
|
|
print(f"Configuration saved to: {output_path}")
|
|
|
|
def print_summary(self):
|
|
"""Print a human-readable summary of the configuration."""
|
|
print("\n" + "="*60)
|
|
print("OPTIMIZATION CONFIGURATION SUMMARY")
|
|
print("="*60)
|
|
|
|
print(f"\nModel: {self.model_info.get('sim_file', 'Unknown')}")
|
|
|
|
print(f"\nDesign Variables ({len(self.config['design_variables'])}):")
|
|
for dv in self.config['design_variables']:
|
|
print(f" • {dv['name']}: [{dv['bounds'][0]:.2f}, {dv['bounds'][1]:.2f}] {dv['units']}")
|
|
|
|
print(f"\nObjectives ({len(self.config['objectives'])}):")
|
|
for obj in self.config['objectives']:
|
|
print(f" • {obj['description']} (weight: {obj['weight']:.1f})")
|
|
|
|
print(f"\nConstraints ({len(self.config['constraints'])}):")
|
|
for const in self.config['constraints']:
|
|
operator = '<=' if const['type'] == 'upper_bound' else '>='
|
|
print(f" • {const['description']}: {const['metric']} {operator} {const['limit']} {const['units']}")
|
|
|
|
print(f"\nOptimization Settings:")
|
|
print(f" • Trials: {self.config['optimization_settings']['n_trials']}")
|
|
print(f" • Sampler: {self.config['optimization_settings']['sampler']}")
|
|
|
|
print("="*60 + "\n")
|
|
|
|
|
|
# Example usage
|
|
if __name__ == "__main__":
|
|
from optimization_engine.model_discovery import discover_fea_model
|
|
|
|
# Step 1: Discover model
|
|
print("Step 1: Discovering FEA model...")
|
|
model_result = discover_fea_model("tests/Bracket_sim1.sim")
|
|
|
|
# Step 2: Create builder
|
|
builder = OptimizationConfigBuilder(model_result)
|
|
|
|
# Step 3: Show available options
|
|
print("\n" + "="*60)
|
|
print("AVAILABLE DESIGN VARIABLES:")
|
|
print("="*60)
|
|
for dv in builder.list_available_design_variables():
|
|
print(f"\n• {dv['name']}")
|
|
print(f" Current value: {dv['current_value']} {dv['units']}")
|
|
print(f" Suggested bounds: {dv['suggested_bounds']}")
|
|
|
|
print("\n" + "="*60)
|
|
print("AVAILABLE OBJECTIVES:")
|
|
print("="*60)
|
|
for key, obj in builder.list_available_objectives().items():
|
|
print(f"\n• {key}")
|
|
print(f" Description: {obj['description']}")
|
|
print(f" Default weight: {obj['typical_weight']}")
|
|
|
|
print("\n" + "="*60)
|
|
print("AVAILABLE CONSTRAINTS:")
|
|
print("="*60)
|
|
for key, const in builder.list_available_constraints().items():
|
|
print(f"\n• {key}")
|
|
print(f" Description: {const['description']}")
|
|
print(f" Typical value: {const['typical_value']} {const['units']}")
|
|
|
|
# Step 4: Build a multi-objective configuration
|
|
print("\n" + "="*60)
|
|
print("BUILDING CONFIGURATION:")
|
|
print("="*60)
|
|
|
|
# Add design variables
|
|
builder.add_design_variable('tip_thickness', 15.0, 25.0)
|
|
builder.add_design_variable('support_angle', 20.0, 40.0)
|
|
builder.add_design_variable('support_blend_radius', 5.0, 15.0)
|
|
|
|
# Add objectives: minimize weight AND minimize stress
|
|
builder.add_objective('minimize_mass', weight=5.0)
|
|
builder.add_objective('minimize_max_stress', weight=10.0)
|
|
|
|
# Add constraints: max displacement < 1.0 mm, max stress < 200 MPa
|
|
builder.add_constraint('max_displacement_limit', limit_value=1.0)
|
|
builder.add_constraint('max_stress_limit', limit_value=200.0)
|
|
|
|
# Set optimization settings
|
|
builder.set_optimization_settings(n_trials=150, sampler='TPE')
|
|
|
|
# Print summary
|
|
builder.print_summary()
|
|
|
|
# Save configuration
|
|
builder.save(Path('optimization_config.json'))
|
|
|
|
print("\nConfiguration ready for optimization!")
|