""" 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 mcp_server.tools.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!")