From 6c30b91a82b8052753840df6c2244e9d7f230c38 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 13:56:41 +0000 Subject: [PATCH] feat: Add optimization configuration builder with multi-objective support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created interactive configuration builder that discovers available options and helps users set up multi-objective optimization with constraints. Features: - Lists all available design variables from discovered model - Provides catalog of objectives (minimize mass, stress, displacement, volume) - Provides catalog of constraints (max stress, max displacement, mass limits) - Suggests reasonable bounds for design variables based on type - Supports multi-objective optimization with configurable weights - Validates and builds complete optimization_config.json Available Objectives: - minimize_mass: Weight reduction (weight: 5.0) - minimize_max_stress: Failure prevention (weight: 10.0) - minimize_max_displacement: Stiffness (weight: 3.0) - minimize_volume: Material usage (weight: 4.0) Available Constraints: - max_stress_limit: Stress <= limit (typical: 200 MPa) - max_displacement_limit: Displacement <= limit (typical: 1.0 mm) - min_mass_limit: Mass >= limit (structural integrity) - max_mass_limit: Mass <= limit (weight budget) Example Configuration: - Design Variables: tip_thickness, support_angle, support_blend_radius - Objectives: Minimize mass (5.0) + Minimize stress (10.0) - Constraints: max_displacement <= 1.0 mm, max_stress <= 200 MPa - Settings: 150 trials, TPE sampler Usage: python optimization_engine/optimization_config_builder.py Output: optimization_config.json with complete multi-objective setup Integration: - Works with discover_fea_model() to find design variables - Links to result extractors (stress, displacement, mass) - Ready for MCP build_optimization_config tool - Supports LLM-driven configuration building This enables the workflow: 1. User: "Minimize weight and stress with max displacement < 1mm" 2. LLM discovers model → lists options → builds config 3. Optimization engine executes with multi-objective + constraints --- optimization_config.json | 190 +++++++++ .../optimization_config_builder.py | 403 ++++++++++++++++++ 2 files changed, 593 insertions(+) create mode 100644 optimization_config.json create mode 100644 optimization_engine/optimization_config_builder.py diff --git a/optimization_config.json b/optimization_config.json new file mode 100644 index 00000000..e3a5489e --- /dev/null +++ b/optimization_config.json @@ -0,0 +1,190 @@ +{ + "design_variables": [ + { + "name": "tip_thickness", + "type": "continuous", + "bounds": [ + 15.0, + 25.0 + ], + "units": "mm", + "initial_value": 20.0 + }, + { + "name": "support_angle", + "type": "continuous", + "bounds": [ + 20.0, + 40.0 + ], + "units": "degrees", + "initial_value": 30.0 + }, + { + "name": "support_blend_radius", + "type": "continuous", + "bounds": [ + 5.0, + 15.0 + ], + "units": "mm", + "initial_value": 10.0 + } + ], + "objectives": [ + { + "name": "minimize_mass", + "description": "Minimize total mass (weight reduction)", + "extractor": "mass_extractor", + "metric": "total_mass", + "direction": "minimize", + "weight": 5.0 + }, + { + "name": "minimize_max_stress", + "description": "Minimize maximum von Mises stress", + "extractor": "stress_extractor", + "metric": "max_von_mises", + "direction": "minimize", + "weight": 10.0 + } + ], + "constraints": [ + { + "name": "max_displacement_limit", + "description": "Maximum allowable displacement", + "extractor": "displacement_extractor", + "metric": "max_displacement", + "type": "upper_bound", + "limit": 1.0, + "units": "mm" + }, + { + "name": "max_stress_limit", + "description": "Maximum allowable von Mises stress", + "extractor": "stress_extractor", + "metric": "max_von_mises", + "type": "upper_bound", + "limit": 200.0, + "units": "MPa" + } + ], + "optimization_settings": { + "n_trials": 150, + "sampler": "TPE", + "n_startup_trials": 20 + }, + "model_info": { + "sim_file": "/home/user/Atomizer/tests/Bracket_sim1.sim", + "solutions": [ + { + "name": "DisableInThermalSolution", + "type": "DisableInThermalSolution", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Disable in Thermal Solution 3D", + "type": "Disable in Thermal Solution 3D", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "-Flow-Structural Coupled Solution Parameters", + "type": "-Flow-Structural Coupled Solution Parameters", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Direct Frequency Response", + "type": "Direct Frequency Response", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "*Thermal-Flow Coupled Solution Parameters", + "type": "*Thermal-Flow Coupled Solution Parameters", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "0Thermal-Structural Coupled Solution Parameters", + "type": "0Thermal-Structural Coupled Solution Parameters", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Linear Statics", + "type": "Linear Statics", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Disable in Thermal Solution 2D", + "type": "Disable in Thermal Solution 2D", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Thermal Solution Parameters", + "type": "Thermal Solution Parameters", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Nonlinear Statics", + "type": "Nonlinear Statics", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Modal Frequency Response", + "type": "Modal Frequency Response", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "1Pass Structural Contact Solution to Flow Solver", + "type": "1Pass Structural Contact Solution to Flow Solver", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "\"ObjectDisableInThermalSolution3D", + "type": "\"ObjectDisableInThermalSolution3D", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Design Optimization", + "type": "Design Optimization", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "\"ObjectDisableInThermalSolution2D", + "type": "\"ObjectDisableInThermalSolution2D", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Normal Modes", + "type": "Normal Modes", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Direct Transient Response", + "type": "Direct Transient Response", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "Modal Transient Response", + "type": "Modal Transient Response", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + } + ] + } +} \ No newline at end of file diff --git a/optimization_engine/optimization_config_builder.py b/optimization_engine/optimization_config_builder.py new file mode 100644 index 00000000..6eed6227 --- /dev/null +++ b/optimization_engine/optimization_config_builder.py @@ -0,0 +1,403 @@ +""" +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!")