diff --git a/LICENSE b/LICENSE index f2879c11..2100d39a 100644 --- a/LICENSE +++ b/LICENSE @@ -9,4 +9,4 @@ property of Atomaste. Unauthorized copying, modification, distribution, or use o Software, via any medium, is strictly prohibited without prior written permission from Atomaste. -For licensing inquiries, please contact: contact@atomaste.com +For licensing inquiries, please contact: antoine@atomaste.ca diff --git a/examples/bracket/optimization_config.json b/examples/bracket/optimization_config.json new file mode 100644 index 00000000..5d8bcc58 --- /dev/null +++ b/examples/bracket/optimization_config.json @@ -0,0 +1,180 @@ +{ + "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 + } + ], + "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": "C:/Users/antoi/Documents/Atomaste/Atomizer/examples/bracket/Bracket_sim1.sim", + "solutions": [ + { + "name": "Direct Frequency Response", + "type": "Direct Frequency Response", + "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": "Nonlinear Statics", + "type": "Nonlinear Statics", + "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": "*Thermal-Flow Coupled Solution Parameters", + "type": "*Thermal-Flow Coupled Solution Parameters", + "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": "Disable in Thermal Solution 3D", + "type": "Disable in Thermal Solution 3D", + "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": "Direct Transient Response", + "type": "Direct Transient Response", + "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": "Normal Modes", + "type": "Normal Modes", + "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" + }, + { + "name": "\"ObjectDisableInThermalSolution3D", + "type": "\"ObjectDisableInThermalSolution3D", + "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": "0Thermal-Structural Coupled Solution Parameters", + "type": "0Thermal-Structural Coupled Solution Parameters", + "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": "DisableInThermalSolution", + "type": "DisableInThermalSolution", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + }, + { + "name": "\"ObjectDisableInThermalSolution2D", + "type": "\"ObjectDisableInThermalSolution2D", + "solver": "NX Nastran", + "description": "Extracted from binary .sim file" + } + ] + } +} \ No newline at end of file diff --git a/mcp_server/tools/README.md b/mcp_server/tools/README.md index 369f37b8..18967d5e 100644 --- a/mcp_server/tools/README.md +++ b/mcp_server/tools/README.md @@ -53,17 +53,77 @@ python mcp_server/tools/model_discovery.py examples/test_bracket.sim --- -### 2. Build Optimization Config (PLANNED) +### 2. Build Optimization Config (`optimization_config.py`) ✅ IMPLEMENTED -**Purpose**: Generate `optimization_config.json` from natural language requirements. +**Purpose**: Generate `optimization_config.json` from user selections of objectives, constraints, and design variables. -**Function**: `build_optimization_config(requirements: str, model_info: Dict) -> Dict[str, Any]` +**Functions**: +- `build_optimization_config(...)` - Create complete optimization configuration +- `list_optimization_options(sim_file_path)` - List all available options for a model +- `format_optimization_options_for_llm(options)` - Format options as Markdown -**Planned Features**: -- Parse LLM instructions ("minimize stress while reducing mass") -- Select appropriate result extractors -- Suggest reasonable parameter bounds -- Generate complete config for optimization engine +**What it does**: +- Discovers available design variables from the FEA model +- Lists available objectives (minimize mass, stress, displacement, volume) +- Lists available constraints (max stress, max displacement, mass limits) +- Builds a complete `optimization_config.json` based on user selections +- Validates that all selections are valid for the model + +**Usage Example**: +```python +from mcp_server.tools import build_optimization_config, list_optimization_options + +# Step 1: List available options +options = list_optimization_options("examples/bracket/Bracket_sim1.sim") +print(f"Available design variables: {len(options['available_design_variables'])}") + +# Step 2: Build configuration +result = build_optimization_config( + sim_file_path="examples/bracket/Bracket_sim1.sim", + design_variables=[ + {'name': 'tip_thickness', 'lower_bound': 15.0, 'upper_bound': 25.0}, + {'name': 'support_angle', 'lower_bound': 20.0, 'upper_bound': 40.0} + ], + objectives=[ + {'objective_key': 'minimize_mass', 'weight': 5.0}, + {'objective_key': 'minimize_max_stress', 'weight': 10.0} + ], + constraints=[ + {'constraint_key': 'max_displacement_limit', 'limit_value': 1.0}, + {'constraint_key': 'max_stress_limit', 'limit_value': 200.0} + ], + optimization_settings={ + 'n_trials': 150, + 'sampler': 'TPE' + } +) + +if result['status'] == 'success': + print(f"Config saved to: {result['config_file']}") +``` + +**Command Line Usage**: +```bash +python mcp_server/tools/optimization_config.py examples/bracket/Bracket_sim1.sim +``` + +**Available Objectives**: +- `minimize_mass`: Minimize total mass (weight reduction) +- `minimize_max_stress`: Minimize maximum von Mises stress +- `minimize_max_displacement`: Minimize maximum displacement (increase stiffness) +- `minimize_volume`: Minimize total volume (material usage) + +**Available Constraints**: +- `max_stress_limit`: Maximum allowable von Mises stress +- `max_displacement_limit`: Maximum allowable displacement +- `min_mass_limit`: Minimum required mass (structural integrity) +- `max_mass_limit`: Maximum allowable mass (weight budget) + +**Output**: Creates `optimization_config.json` with: +- Design variable definitions with bounds +- Multi-objective configuration with weights +- Constraint definitions with limits +- Optimization algorithm settings (trials, sampler) --- @@ -218,4 +278,4 @@ These tools are designed to be called by the MCP server and consumed by LLMs. Th --- **Last Updated**: 2025-11-15 -**Status**: Phase 1 (Model Discovery) ✅ COMPLETE +**Status**: Phase 1 (Model Discovery) ✅ COMPLETE | Phase 2 (Optimization Config Builder) ✅ COMPLETE diff --git a/mcp_server/tools/__init__.py b/mcp_server/tools/__init__.py index 33ae5fbe..6a8b5792 100644 --- a/mcp_server/tools/__init__.py +++ b/mcp_server/tools/__init__.py @@ -13,11 +13,18 @@ Available tools: from typing import Dict, Any from .model_discovery import discover_fea_model, format_discovery_result_for_llm +from .optimization_config import ( + build_optimization_config, + list_optimization_options, + format_optimization_options_for_llm +) __all__ = [ "discover_fea_model", "format_discovery_result_for_llm", "build_optimization_config", + "list_optimization_options", + "format_optimization_options_for_llm", "start_optimization", "query_optimization_status", "extract_results", diff --git a/mcp_server/tools/optimization_config.py b/mcp_server/tools/optimization_config.py new file mode 100644 index 00000000..3a70adce --- /dev/null +++ b/mcp_server/tools/optimization_config.py @@ -0,0 +1,368 @@ +""" +MCP Tool: Build Optimization Configuration + +Wraps the OptimizationConfigBuilder to create an MCP-compatible tool +that helps LLMs guide users through building optimization configurations. + +This tool: +1. Discovers the FEA model (design variables) +2. Lists available objectives and constraints +3. Builds a complete optimization_config.json based on user selections +""" + +from pathlib import Path +from typing import Dict, Any, List, Optional +import json +import sys + +# Add project root to path for imports +project_root = Path(__file__).parent.parent.parent +sys.path.insert(0, str(project_root)) + +from optimization_engine.optimization_config_builder import OptimizationConfigBuilder +from mcp_server.tools.model_discovery import discover_fea_model + + +def build_optimization_config( + sim_file_path: str, + design_variables: List[Dict[str, Any]], + objectives: List[Dict[str, Any]], + constraints: Optional[List[Dict[str, Any]]] = None, + optimization_settings: Optional[Dict[str, Any]] = None, + output_path: Optional[str] = None +) -> Dict[str, Any]: + """ + MCP Tool: Build Optimization Configuration + + Creates a complete optimization configuration file from user selections. + + Args: + sim_file_path: Absolute path to .sim file + design_variables: List of design variable definitions + [ + { + 'name': 'tip_thickness', + 'lower_bound': 15.0, + 'upper_bound': 25.0 + }, + ... + ] + objectives: List of objective definitions + [ + { + 'objective_key': 'minimize_mass', + 'weight': 5.0, # optional + 'target': None # optional, for goal programming + }, + ... + ] + constraints: Optional list of constraint definitions + [ + { + 'constraint_key': 'max_stress_limit', + 'limit_value': 200.0 + }, + ... + ] + optimization_settings: Optional dict with algorithm settings + { + 'n_trials': 100, + 'sampler': 'TPE' + } + output_path: Optional path to save config JSON. + Defaults to 'optimization_config.json' in sim file directory + + Returns: + Dictionary with status and configuration details + + Example: + >>> result = build_optimization_config( + ... sim_file_path="C:/Projects/Bracket/analysis.sim", + ... design_variables=[ + ... {'name': 'tip_thickness', 'lower_bound': 15.0, 'upper_bound': 25.0} + ... ], + ... objectives=[ + ... {'objective_key': 'minimize_mass', 'weight': 5.0} + ... ], + ... constraints=[ + ... {'constraint_key': 'max_stress_limit', 'limit_value': 200.0} + ... ] + ... ) + """ + try: + # Step 1: Discover model + model_result = discover_fea_model(sim_file_path) + + if model_result['status'] != 'success': + return { + 'status': 'error', + 'error_type': 'model_discovery_failed', + 'message': model_result.get('message', 'Failed to discover FEA model'), + 'suggestion': model_result.get('suggestion', 'Check that the .sim file is valid') + } + + # Step 2: Create builder + builder = OptimizationConfigBuilder(model_result) + + # Step 3: Validate and add design variables + available_vars = {dv['name']: dv for dv in builder.list_available_design_variables()} + + for dv in design_variables: + name = dv['name'] + if name not in available_vars: + return { + 'status': 'error', + 'error_type': 'invalid_design_variable', + 'message': f"Design variable '{name}' not found in model", + 'available_variables': list(available_vars.keys()), + 'suggestion': f"Choose from: {', '.join(available_vars.keys())}" + } + + builder.add_design_variable( + name=name, + lower_bound=dv['lower_bound'], + upper_bound=dv['upper_bound'] + ) + + # Step 4: Add objectives + available_objectives = builder.list_available_objectives() + + for obj in objectives: + obj_key = obj['objective_key'] + if obj_key not in available_objectives: + return { + 'status': 'error', + 'error_type': 'invalid_objective', + 'message': f"Objective '{obj_key}' not recognized", + 'available_objectives': list(available_objectives.keys()), + 'suggestion': f"Choose from: {', '.join(available_objectives.keys())}" + } + + builder.add_objective( + objective_key=obj_key, + weight=obj.get('weight'), + target=obj.get('target') + ) + + # Step 5: Add constraints (optional) + if constraints: + available_constraints = builder.list_available_constraints() + + for const in constraints: + const_key = const['constraint_key'] + if const_key not in available_constraints: + return { + 'status': 'error', + 'error_type': 'invalid_constraint', + 'message': f"Constraint '{const_key}' not recognized", + 'available_constraints': list(available_constraints.keys()), + 'suggestion': f"Choose from: {', '.join(available_constraints.keys())}" + } + + builder.add_constraint( + constraint_key=const_key, + limit_value=const['limit_value'] + ) + + # Step 6: Set optimization settings (optional) + if optimization_settings: + builder.set_optimization_settings( + n_trials=optimization_settings.get('n_trials'), + sampler=optimization_settings.get('sampler') + ) + + # Step 7: Build and validate configuration + config = builder.build() + + # Step 8: Save to file + if output_path is None: + sim_path = Path(sim_file_path) + output_path = sim_path.parent / 'optimization_config.json' + else: + output_path = Path(output_path) + + with open(output_path, 'w') as f: + json.dump(config, f, indent=2) + + # Step 9: Return success with summary + return { + 'status': 'success', + 'message': 'Optimization configuration created successfully', + 'config_file': str(output_path), + 'summary': { + 'design_variables': len(config['design_variables']), + 'objectives': len(config['objectives']), + 'constraints': len(config['constraints']), + 'n_trials': config['optimization_settings']['n_trials'], + 'sampler': config['optimization_settings']['sampler'] + }, + 'config': config + } + + except ValueError as e: + return { + 'status': 'error', + 'error_type': 'validation_error', + 'message': str(e), + 'suggestion': 'Check that all required fields are provided correctly' + } + + except Exception as e: + return { + 'status': 'error', + 'error_type': 'unexpected_error', + 'message': str(e), + 'suggestion': 'This may be a bug. Please report this issue.' + } + + +def list_optimization_options(sim_file_path: str) -> Dict[str, Any]: + """ + Helper tool: List all available optimization options for a model. + + This is useful for LLMs to show users what they can choose from. + + Args: + sim_file_path: Absolute path to .sim file + + Returns: + Dictionary with all available options + """ + try: + # Discover model + model_result = discover_fea_model(sim_file_path) + + if model_result['status'] != 'success': + return model_result + + # Create builder to get options + builder = OptimizationConfigBuilder(model_result) + + # Get all available options + design_vars = builder.list_available_design_variables() + objectives = builder.list_available_objectives() + constraints = builder.list_available_constraints() + + return { + 'status': 'success', + 'sim_file': sim_file_path, + 'available_design_variables': design_vars, + 'available_objectives': objectives, + 'available_constraints': constraints, + 'model_info': { + 'solutions': model_result.get('solutions', []), + 'expression_count': len(model_result.get('expressions', [])) + } + } + + except Exception as e: + return { + 'status': 'error', + 'error_type': 'unexpected_error', + 'message': str(e) + } + + +def format_optimization_options_for_llm(options: Dict[str, Any]) -> str: + """ + Format optimization options for LLM consumption (Markdown). + + Args: + options: Output from list_optimization_options() + + Returns: + Markdown-formatted string + """ + if options['status'] != 'success': + return f"❌ **Error**: {options['message']}\n\n💡 {options.get('suggestion', '')}" + + md = [] + md.append(f"# Optimization Configuration Options\n") + md.append(f"**Model**: `{options['sim_file']}`\n") + + # Design Variables + md.append(f"## Available Design Variables ({len(options['available_design_variables'])})\n") + if options['available_design_variables']: + md.append("| Name | Current Value | Units | Suggested Bounds |") + md.append("|------|---------------|-------|------------------|") + for dv in options['available_design_variables']: + bounds = dv['suggested_bounds'] + md.append(f"| `{dv['name']}` | {dv['current_value']} | {dv['units']} | [{bounds[0]:.2f}, {bounds[1]:.2f}] |") + else: + md.append("⚠️ No design variables found. Model may not be parametric.") + md.append("") + + # Objectives + md.append(f"## Available Objectives\n") + for key, obj in options['available_objectives'].items(): + md.append(f"### `{key}`") + md.append(f"- **Description**: {obj['description']}") + md.append(f"- **Metric**: {obj['metric']} ({obj['units']})") + md.append(f"- **Default Weight**: {obj['typical_weight']}") + md.append(f"- **Extractor**: `{obj['extractor']}`") + md.append("") + + # Constraints + md.append(f"## Available Constraints\n") + for key, const in options['available_constraints'].items(): + md.append(f"### `{key}`") + md.append(f"- **Description**: {const['description']}") + md.append(f"- **Metric**: {const['metric']} ({const['units']})") + md.append(f"- **Typical Value**: {const['typical_value']}") + md.append(f"- **Type**: {const['constraint_type']}") + md.append(f"- **Extractor**: `{const['extractor']}`") + md.append("") + + return "\n".join(md) + + +# For testing +if __name__ == "__main__": + import sys + + if len(sys.argv) < 2: + print("Usage: python optimization_config.py ") + sys.exit(1) + + sim_path = sys.argv[1] + + # Test 1: List options + print("=" * 60) + print("TEST 1: List Available Options") + print("=" * 60) + options = list_optimization_options(sim_path) + print(format_optimization_options_for_llm(options)) + + # Test 2: Build configuration + print("\n" + "=" * 60) + print("TEST 2: Build Optimization Configuration") + print("=" * 60) + + result = build_optimization_config( + sim_file_path=sim_path, + design_variables=[ + {'name': 'tip_thickness', 'lower_bound': 15.0, 'upper_bound': 25.0}, + {'name': 'support_angle', 'lower_bound': 20.0, 'upper_bound': 40.0}, + ], + objectives=[ + {'objective_key': 'minimize_mass', 'weight': 5.0}, + {'objective_key': 'minimize_max_stress', 'weight': 10.0} + ], + constraints=[ + {'constraint_key': 'max_displacement_limit', 'limit_value': 1.0}, + {'constraint_key': 'max_stress_limit', 'limit_value': 200.0} + ], + optimization_settings={ + 'n_trials': 150, + 'sampler': 'TPE' + } + ) + + if result['status'] == 'success': + print(f"SUCCESS: Configuration saved to: {result['config_file']}") + print(f"\nSummary:") + for key, value in result['summary'].items(): + print(f" - {key}: {value}") + else: + print(f"ERROR: {result['message']}") + print(f"Suggestion: {result.get('suggestion', '')}")