feat: Add optimization configuration builder with multi-objective support

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
This commit is contained in:
Claude
2025-11-15 13:56:41 +00:00
parent 16cddd5243
commit 6c30b91a82
2 changed files with 593 additions and 0 deletions

190
optimization_config.json Normal file
View File

@@ -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"
}
]
}
}

View File

@@ -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!")