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/examples/test_bracket.sim b/examples/test_bracket.sim
new file mode 100644
index 00000000..def626ce
--- /dev/null
+++ b/examples/test_bracket.sim
@@ -0,0 +1,80 @@
+
+
+
+
+
+ test_bracket
+ Simple bracket structural analysis
+ NX 2412
+ 2025-11-15
+
+
+
+
+
+ Linear static analysis under load
+
+ 101
+ Direct
+
+
+
+
+
+
+
+ 5.0
+ Dimension
+
+
+ 10.0
+ Dimension
+
+
+ 40.0
+ Dimension
+
+
+ 2.7
+ Material Property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Top Face
+ 0 -1 0
+
+
+
+
+
+ Bottom Holes
+
+
+
+
+
+
+ test_bracket.prt
+ test_bracket.fem
+
+
diff --git a/mcp_server/tools/README.md b/mcp_server/tools/README.md
new file mode 100644
index 00000000..18967d5e
--- /dev/null
+++ b/mcp_server/tools/README.md
@@ -0,0 +1,281 @@
+# MCP Tools Documentation
+
+This directory contains the MCP (Model Context Protocol) tools that enable LLM-driven optimization configuration for Atomizer.
+
+## Available Tools
+
+### 1. Model Discovery (`model_discovery.py`) ✅ IMPLEMENTED
+
+**Purpose**: Parse Siemens NX .sim files to extract FEA model information.
+
+**Function**: `discover_fea_model(sim_file_path: str) -> Dict[str, Any]`
+
+**What it extracts**:
+- **Solutions**: Analysis types (static, thermal, modal, etc.)
+- **Expressions**: Parametric variables that can be optimized
+- **FEM Info**: Mesh, materials, loads, constraints
+- **Linked Files**: Associated .prt files and result files
+
+**Usage Example**:
+```python
+from mcp_server.tools import discover_fea_model, format_discovery_result_for_llm
+
+# Discover model
+result = discover_fea_model("C:/Projects/Bracket/analysis.sim")
+
+# Format for LLM
+if result['status'] == 'success':
+ markdown_output = format_discovery_result_for_llm(result)
+ print(markdown_output)
+
+# Access structured data
+for expr in result['expressions']:
+ print(f"{expr['name']}: {expr['value']} {expr['units']}")
+```
+
+**Command Line Usage**:
+```bash
+python mcp_server/tools/model_discovery.py examples/test_bracket.sim
+```
+
+**Output Format**:
+- **JSON**: Complete structured data for programmatic use
+- **Markdown**: Human-readable format for LLM consumption
+
+**Supported .sim File Versions**:
+- NX 2412 (tested)
+- Should work with NX 12.0+ (XML-based .sim files)
+
+**Limitations**:
+- Expression values are best-effort extracted from .sim XML
+- For accurate values, the associated .prt file is parsed (binary parsing)
+- Binary .prt parsing is heuristic-based and may miss some expressions
+
+---
+
+### 2. Build Optimization Config (`optimization_config.py`) ✅ IMPLEMENTED
+
+**Purpose**: Generate `optimization_config.json` from user selections of objectives, constraints, and design variables.
+
+**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
+
+**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)
+
+---
+
+### 3. Start Optimization (PLANNED)
+
+**Purpose**: Launch optimization run with given configuration.
+
+**Function**: `start_optimization(config_path: str, resume: bool = False) -> Dict[str, Any]`
+
+---
+
+### 4. Query Optimization Status (PLANNED)
+
+**Purpose**: Get current status of running optimization.
+
+**Function**: `query_optimization_status(session_id: str) -> Dict[str, Any]`
+
+---
+
+### 5. Extract Results (PLANNED)
+
+**Purpose**: Parse FEA result files (OP2, F06, XDB) for optimization metrics.
+
+**Function**: `extract_results(result_files: List[str], extractors: List[str]) -> Dict[str, Any]`
+
+---
+
+### 6. Run NX Journal (PLANNED)
+
+**Purpose**: Execute NXOpen scripts via file-based communication.
+
+**Function**: `run_nx_journal(journal_script: str, parameters: Dict) -> Dict[str, Any]`
+
+---
+
+## Testing
+
+### Unit Tests
+
+```bash
+# Install pytest (if not already installed)
+pip install pytest
+
+# Run all MCP tool tests
+pytest tests/mcp_server/tools/ -v
+
+# Run specific test
+pytest tests/mcp_server/tools/test_model_discovery.py -v
+```
+
+### Example Files
+
+Example .sim files for testing are located in `examples/`:
+- `test_bracket.sim`: Simple structural analysis with 4 expressions
+
+---
+
+## Development Guidelines
+
+### Adding a New Tool
+
+1. **Create module**: `mcp_server/tools/your_tool.py`
+
+2. **Implement function**:
+```python
+def your_tool_name(param: str) -> Dict[str, Any]:
+ """
+ Brief description.
+
+ Args:
+ param: Description
+
+ Returns:
+ Structured result dictionary
+ """
+ try:
+ # Implementation
+ return {
+ 'status': 'success',
+ 'data': result
+ }
+ except Exception as e:
+ return {
+ 'status': 'error',
+ 'error_type': 'error_category',
+ 'message': str(e),
+ 'suggestion': 'How to fix'
+ }
+```
+
+3. **Add to `__init__.py`**:
+```python
+from .your_tool import your_tool_name
+
+__all__ = [
+ # ... existing tools
+ "your_tool_name",
+]
+```
+
+4. **Create tests**: `tests/mcp_server/tools/test_your_tool.py`
+
+5. **Update documentation**: Add section to this README
+
+---
+
+## Error Handling
+
+All MCP tools follow a consistent error handling pattern:
+
+**Success Response**:
+```json
+{
+ "status": "success",
+ "data": { ... }
+}
+```
+
+**Error Response**:
+```json
+{
+ "status": "error",
+ "error_type": "file_not_found | invalid_file | unexpected_error",
+ "message": "Detailed error message",
+ "suggestion": "Actionable suggestion for user"
+}
+```
+
+---
+
+## Integration with MCP Server
+
+These tools are designed to be called by the MCP server and consumed by LLMs. The workflow is:
+
+1. **LLM Request**: "Analyze my FEA model at C:/Projects/model.sim"
+2. **MCP Server**: Calls `discover_fea_model()`
+3. **Tool Returns**: Structured JSON result
+4. **MCP Server**: Formats with `format_discovery_result_for_llm()`
+5. **LLM Response**: Uses formatted data to answer user
+
+---
+
+## Future Enhancements
+
+- [ ] Support for binary .sim file formats (older NX versions)
+- [ ] Direct NXOpen integration for accurate expression extraction
+- [ ] Support for additional analysis types (thermal, modal, etc.)
+- [ ] Caching of parsed results for performance
+- [ ] Validation of .sim file integrity
+- [ ] Extraction of solver convergence settings
+
+---
+
+**Last Updated**: 2025-11-15
+**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 80d0267f..6a8b5792 100644
--- a/mcp_server/tools/__init__.py
+++ b/mcp_server/tools/__init__.py
@@ -12,10 +12,19 @@ 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/model_discovery.py b/mcp_server/tools/model_discovery.py
new file mode 100644
index 00000000..9425c779
--- /dev/null
+++ b/mcp_server/tools/model_discovery.py
@@ -0,0 +1,621 @@
+"""
+MCP Tool: FEA Model Discovery
+
+Parses Siemens NX .sim files to extract:
+- Simulation solutions (structural, thermal, modal, etc.)
+- Parametric expressions (design variables)
+- FEM information (mesh, elements, materials)
+- Linked part files
+
+This tool enables LLM-driven optimization configuration by providing
+structured information about what can be optimized in a given FEA model.
+"""
+
+import xml.etree.ElementTree as ET
+from pathlib import Path
+from typing import Dict, Any, List, Optional
+import json
+import re
+
+
+class SimFileParser:
+ """
+ Parser for Siemens NX .sim (simulation) files.
+
+ IMPORTANT: Real NX .sim files are BINARY (not XML) in NX 12+.
+ The parser uses two approaches:
+ 1. XML parsing for test/legacy files
+ 2. Binary string extraction for real NX files
+
+ .sim files contain references to:
+ - Parent .prt file (geometry and expressions)
+ - Solution definitions (structural, thermal, etc.)
+ - FEM (mesh, materials, loads, constraints)
+ - Solver settings
+ """
+
+ def __init__(self, sim_path: Path):
+ """
+ Initialize parser with path to .sim file.
+
+ Args:
+ sim_path: Absolute path to .sim file
+
+ Raises:
+ FileNotFoundError: If sim file doesn't exist
+ ValueError: If file is not a valid .sim file
+ """
+ self.sim_path = Path(sim_path)
+
+ if not self.sim_path.exists():
+ raise FileNotFoundError(f"Sim file not found: {sim_path}")
+
+ if self.sim_path.suffix.lower() != '.sim':
+ raise ValueError(f"Not a .sim file: {sim_path}")
+
+ self.tree = None
+ self.root = None
+ self.is_binary = False
+ self.sim_strings = [] # Extracted strings from binary file
+ self._parse_file()
+
+ def _parse_file(self):
+ """
+ Parse the .sim file - handles both XML (test files) and binary (real NX files).
+ """
+ # First, try XML parsing
+ try:
+ self.tree = ET.parse(self.sim_path)
+ self.root = self.tree.getroot()
+ self.is_binary = False
+ return
+ except ET.ParseError:
+ # Not XML, must be binary - this is normal for real NX files
+ pass
+
+ # Binary file - extract readable strings
+ try:
+ with open(self.sim_path, 'rb') as f:
+ content = f.read()
+
+ # Extract strings (sequences of printable ASCII characters)
+ # Minimum length of 4 to avoid noise
+ text_content = content.decode('latin-1', errors='ignore')
+ self.sim_strings = re.findall(r'[\x20-\x7E]{4,}', text_content)
+ self.is_binary = True
+
+ except Exception as e:
+ raise ValueError(f"Failed to parse .sim file (tried both XML and binary): {e}")
+
+ def extract_solutions(self) -> List[Dict[str, Any]]:
+ """
+ Extract solution definitions from .sim file.
+
+ Returns:
+ List of solution dictionaries with type, name, solver info
+ """
+ solutions = []
+
+ if not self.is_binary and self.root is not None:
+ # XML parsing
+ for solution_tag in ['Solution', 'AnalysisSolution', 'SimSolution']:
+ for elem in self.root.iter(solution_tag):
+ solution_info = {
+ 'name': elem.get('name', 'Unknown'),
+ 'type': elem.get('type', 'Unknown'),
+ 'solver': elem.get('solver', 'NX Nastran'),
+ 'description': elem.get('description', ''),
+ }
+ solutions.append(solution_info)
+ else:
+ # Binary parsing - look for solution type indicators
+ solution_types = {
+ 'SOL 101': 'Linear Statics',
+ 'SOL 103': 'Normal Modes',
+ 'SOL 106': 'Nonlinear Statics',
+ 'SOL 108': 'Direct Frequency Response',
+ 'SOL 109': 'Direct Transient Response',
+ 'SOL 111': 'Modal Frequency Response',
+ 'SOL 112': 'Modal Transient Response',
+ 'SOL 200': 'Design Optimization',
+ }
+
+ found_solutions = set()
+ for s in self.sim_strings:
+ for sol_id, sol_type in solution_types.items():
+ if sol_id in s:
+ found_solutions.add(sol_type)
+
+ # Also check for solution names in strings
+ for s in self.sim_strings:
+ if 'Solution' in s and len(s) < 50:
+ # Potential solution name
+ if any(word in s for word in ['Structural', 'Thermal', 'Modal', 'Static']):
+ found_solutions.add(s.strip())
+
+ for sol_name in found_solutions:
+ solutions.append({
+ 'name': sol_name,
+ 'type': sol_name,
+ 'solver': 'NX Nastran',
+ 'description': 'Extracted from binary .sim file'
+ })
+
+ # Default if nothing found
+ if not solutions:
+ solutions.append({
+ 'name': 'Default Solution',
+ 'type': 'Static Structural',
+ 'solver': 'NX Nastran',
+ 'description': 'Solution info could not be fully extracted from .sim file'
+ })
+
+ return solutions
+
+ def extract_expressions(self) -> List[Dict[str, Any]]:
+ """
+ Extract expression references from .sim file.
+
+ Note: Actual expression values are stored in the .prt file.
+ This method extracts references and attempts to read from .prt if available.
+
+ Returns:
+ List of expression dictionaries with name, value, units
+ """
+ expressions = []
+
+ # XML parsing - look for expression elements
+ if not self.is_binary and self.root is not None:
+ for expr_elem in self.root.iter('Expression'):
+ expr_info = {
+ 'name': expr_elem.get('name', ''),
+ 'value': expr_elem.get('value', None),
+ 'units': expr_elem.get('units', ''),
+ 'formula': expr_elem.text if expr_elem.text else None
+ }
+ if expr_info['name']:
+ expressions.append(expr_info)
+
+ # Try to read from associated .prt file (works for both XML and binary .sim)
+ # Try multiple naming patterns:
+ # 1. Same name as .sim: Bracket_sim1.prt
+ # 2. Base name: Bracket.prt
+ # 3. With _i suffix: Bracket_fem1_i.prt
+ prt_paths = [
+ self.sim_path.with_suffix('.prt'), # Bracket_sim1.prt
+ self.sim_path.parent / f"{self.sim_path.stem.split('_')[0]}.prt", # Bracket.prt
+ self.sim_path.parent / f"{self.sim_path.stem}_i.prt", # Bracket_sim1_i.prt
+ ]
+
+ for prt_path in prt_paths:
+ if prt_path.exists():
+ prt_expressions = self._extract_prt_expressions(prt_path)
+ # Merge with existing, prioritizing .prt values
+ expr_dict = {e['name']: e for e in expressions}
+ for prt_expr in prt_expressions:
+ expr_dict[prt_expr['name']] = prt_expr
+ expressions = list(expr_dict.values())
+ break # Use first .prt file found
+
+ return expressions
+
+ def _extract_prt_expressions(self, prt_path: Path) -> List[Dict[str, Any]]:
+ """
+ Extract expressions from associated .prt file.
+
+ .prt files are binary, but expression data is stored in readable sections.
+ NX expression format: #(Type [units]) name: value;
+
+ Args:
+ prt_path: Path to .prt file
+
+ Returns:
+ List of expression dictionaries
+ """
+ expressions = []
+
+ try:
+ # Read as binary and search for text patterns
+ with open(prt_path, 'rb') as f:
+ content = f.read()
+
+ # Try to decode as latin-1 (preserves all byte values)
+ text_content = content.decode('latin-1', errors='ignore')
+
+ # Pattern 1: NX native format with variations:
+ # #(Number [mm]) tip_thickness: 20;
+ # (Number [mm]) p3: 10;
+ # *(Number [mm]) support_blend_radius: 10;
+ # ((Number [degrees]) support_angle: 30;
+ # Prefix can be: #(, *(, (, ((
+ nx_pattern = r'[#*\(]*\((\w+)\s*\[([^\]]*)\]\)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)'
+
+ # Use set to avoid duplicates
+ expr_names_seen = set()
+
+ for match in re.finditer(nx_pattern, text_content):
+ expr_type, units, name, value = match.groups()
+ if name not in expr_names_seen:
+ expr_names_seen.add(name)
+ expressions.append({
+ 'name': name,
+ 'value': float(value),
+ 'units': units,
+ 'type': expr_type,
+ 'source': 'prt_file_nx_format'
+ })
+
+ # Pattern 2: Find expression names from Root: references
+ # Format: Root:expression_name:
+ root_pattern = r'Root:([a-zA-Z_][a-zA-Z0-9_]{2,}):'
+ potential_expr_names = set()
+
+ for match in re.finditer(root_pattern, text_content):
+ name = match.group(1)
+ # Filter out common NX internal names
+ if name not in ['index', '%%Name', '%%ug_objects_for_', 'WorldModifier']:
+ if not name.startswith('%%'):
+ potential_expr_names.add(name)
+
+ # For names found in Root: but not in value patterns,
+ # mark as "found but value unknown"
+ for name in potential_expr_names:
+ if name not in expr_names_seen:
+ expressions.append({
+ 'name': name,
+ 'value': None,
+ 'units': '',
+ 'type': 'Unknown',
+ 'source': 'prt_file_reference_only'
+ })
+
+ # Pattern 3: Fallback - simple name=value pattern
+ # Only use if no NX-format expressions found
+ if not expressions:
+ simple_pattern = r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)'
+
+ for match in re.finditer(simple_pattern, text_content):
+ name, value = match.groups()
+ # Filter out common false positives (short names, underscore-prefixed)
+ if len(name) > 3 and not name.startswith('_'):
+ # Additional filter: avoid Nastran keywords
+ if name.upper() not in ['PRINT', 'PUNCH', 'PLOT', 'BOTH', 'GRID', 'GAUSS']:
+ expressions.append({
+ 'name': name,
+ 'value': float(value),
+ 'units': '',
+ 'source': 'prt_file_simple_pattern'
+ })
+
+ except Exception as e:
+ # .prt parsing is best-effort, don't fail if it doesn't work
+ print(f"Warning: Could not extract expressions from .prt file: {e}")
+
+ return expressions
+
+ def extract_fem_info(self) -> Dict[str, Any]:
+ """
+ Extract FEM (finite element model) information.
+
+ Returns:
+ Dictionary with mesh, material, and element info
+ """
+ fem_info = {
+ 'mesh': {},
+ 'materials': [],
+ 'element_types': [],
+ 'loads': [],
+ 'constraints': []
+ }
+
+ if not self.is_binary and self.root is not None:
+ # XML parsing
+ for mesh_elem in self.root.iter('Mesh'):
+ fem_info['mesh'] = {
+ 'name': mesh_elem.get('name', 'Default Mesh'),
+ 'element_size': mesh_elem.get('element_size', 'Unknown'),
+ 'node_count': mesh_elem.get('node_count', 'Unknown'),
+ 'element_count': mesh_elem.get('element_count', 'Unknown')
+ }
+
+ for mat_elem in self.root.iter('Material'):
+ material = {
+ 'name': mat_elem.get('name', 'Unknown'),
+ 'type': mat_elem.get('type', 'Isotropic'),
+ 'properties': {}
+ }
+ for prop in ['youngs_modulus', 'poissons_ratio', 'density', 'yield_strength']:
+ if mat_elem.get(prop):
+ material['properties'][prop] = mat_elem.get(prop)
+ fem_info['materials'].append(material)
+
+ for elem_type in self.root.iter('ElementType'):
+ fem_info['element_types'].append(elem_type.get('type', 'Unknown'))
+
+ for load_elem in self.root.iter('Load'):
+ load = {
+ 'name': load_elem.get('name', 'Unknown'),
+ 'type': load_elem.get('type', 'Force'),
+ 'magnitude': load_elem.get('magnitude', 'Unknown')
+ }
+ fem_info['loads'].append(load)
+
+ for constraint_elem in self.root.iter('Constraint'):
+ constraint = {
+ 'name': constraint_elem.get('name', 'Unknown'),
+ 'type': constraint_elem.get('type', 'Fixed'),
+ }
+ fem_info['constraints'].append(constraint)
+
+ else:
+ # Binary parsing - extract from .fem file if available
+ fem_path = self.sim_path.with_name(self.sim_path.stem.replace('_sim', '_fem') + '.fem')
+ if not fem_path.exists():
+ # Try alternative naming patterns
+ fem_path = self.sim_path.parent / f"{self.sim_path.stem.split('_')[0]}_fem1.fem"
+
+ if fem_path.exists():
+ fem_info = self._extract_fem_from_fem_file(fem_path)
+ else:
+ # Extract what we can from .sim strings
+ fem_info['note'] = 'Limited FEM info available from binary .sim file'
+
+ return fem_info
+
+ def _extract_fem_from_fem_file(self, fem_path: Path) -> Dict[str, Any]:
+ """
+ Extract FEM information from .fem file.
+
+ Args:
+ fem_path: Path to .fem file
+
+ Returns:
+ Dictionary with FEM information
+ """
+ fem_info = {
+ 'mesh': {},
+ 'materials': [],
+ 'element_types': set(),
+ 'loads': [],
+ 'constraints': []
+ }
+
+ try:
+ with open(fem_path, 'rb') as f:
+ content = f.read()
+ text_content = content.decode('latin-1', errors='ignore')
+
+ # Look for mesh metadata
+ mesh_match = re.search(r'Mesh\s+(\d+)', text_content)
+ if mesh_match:
+ fem_info['mesh']['name'] = f"Mesh {mesh_match.group(1)}"
+
+ # Look for material names
+ for material_match in re.finditer(r'MAT\d+\s+([A-Za-z0-9_\-\s]+)', text_content):
+ mat_name = material_match.group(1).strip()
+ if mat_name and len(mat_name) > 2:
+ fem_info['materials'].append({
+ 'name': mat_name,
+ 'type': 'Unknown',
+ 'properties': {}
+ })
+
+ # Look for element types (Nastran format: CQUAD4, CTRIA3, CTETRA, etc.)
+ element_pattern = r'\b(C[A-Z]{3,6}\d?)\b'
+ for elem_match in re.finditer(element_pattern, text_content):
+ elem_type = elem_match.group(1)
+ if elem_type.startswith('C') and len(elem_type) <= 8:
+ fem_info['element_types'].add(elem_type)
+
+ fem_info['element_types'] = list(fem_info['element_types'])
+
+ except Exception as e:
+ fem_info['note'] = f'Could not fully parse .fem file: {e}'
+
+ return fem_info
+
+ def get_linked_files(self) -> Dict[str, str]:
+ """
+ Get paths to linked files (.prt, result files, etc.)
+
+ Returns:
+ Dictionary mapping file type to path
+ """
+ linked_files = {}
+
+ # .prt file (geometry and expressions)
+ prt_path = self.sim_path.with_suffix('.prt')
+ if prt_path.exists():
+ linked_files['part_file'] = str(prt_path)
+
+ # Common result file locations
+ result_dir = self.sim_path.parent
+ sim_name = self.sim_path.stem
+
+ # Nastran result files
+ for ext in ['.op2', '.f06', '.f04', '.bdf']:
+ result_file = result_dir / f"{sim_name}{ext}"
+ if result_file.exists():
+ linked_files[f'result{ext}'] = str(result_file)
+
+ return linked_files
+
+
+def discover_fea_model(sim_file_path: str) -> Dict[str, Any]:
+ """
+ MCP Tool: Discover FEA Model
+
+ Analyzes a Siemens NX .sim file and extracts:
+ - Solutions (analysis types)
+ - Expressions (potential design variables)
+ - FEM information (mesh, materials, loads)
+ - Linked files
+
+ This is the primary tool for LLM-driven optimization setup.
+
+ Args:
+ sim_file_path: Absolute path to .sim file (Windows or Unix format)
+
+ Returns:
+ Structured dictionary with model information
+
+ Example:
+ >>> result = discover_fea_model("C:/Projects/Bracket/analysis.sim")
+ >>> print(result['expressions'])
+ [{'name': 'wall_thickness', 'value': 5.0, 'units': 'mm'}, ...]
+ """
+ try:
+ # Normalize path (handle both Windows and Unix)
+ sim_path = Path(sim_file_path).resolve()
+
+ # Parse the .sim file
+ parser = SimFileParser(sim_path)
+
+ # Extract all components
+ result = {
+ 'status': 'success',
+ 'sim_file': str(sim_path),
+ 'file_exists': sim_path.exists(),
+ 'solutions': parser.extract_solutions(),
+ 'expressions': parser.extract_expressions(),
+ 'fem_info': parser.extract_fem_info(),
+ 'linked_files': parser.get_linked_files(),
+ 'metadata': {
+ 'parser_version': '0.1.0',
+ 'nx_version': 'NX 2412', # Can be extracted from .sim file in future
+ }
+ }
+
+ # Add summary statistics
+ result['summary'] = {
+ 'solution_count': len(result['solutions']),
+ 'expression_count': len(result['expressions']),
+ 'material_count': len(result['fem_info']['materials']),
+ 'load_count': len(result['fem_info']['loads']),
+ 'constraint_count': len(result['fem_info']['constraints']),
+ }
+
+ return result
+
+ except FileNotFoundError as e:
+ return {
+ 'status': 'error',
+ 'error_type': 'file_not_found',
+ 'message': str(e),
+ 'suggestion': 'Check that the file path is absolute and the .sim file exists'
+ }
+
+ except ValueError as e:
+ return {
+ 'status': 'error',
+ 'error_type': 'invalid_file',
+ 'message': str(e),
+ 'suggestion': 'Ensure the file is a valid NX .sim file (not corrupted or encrypted)'
+ }
+
+ except Exception as e:
+ return {
+ 'status': 'error',
+ 'error_type': 'unexpected_error',
+ 'message': str(e),
+ 'suggestion': 'This may be an unsupported .sim file format. Please report this issue.'
+ }
+
+
+def format_discovery_result_for_llm(result: Dict[str, Any]) -> str:
+ """
+ Format discovery result for LLM consumption (Markdown).
+
+ This is used by the MCP server to present results to the LLM
+ in a clear, structured format.
+
+ Args:
+ result: Output from discover_fea_model()
+
+ Returns:
+ Markdown-formatted string
+ """
+ if result['status'] != 'success':
+ return f"❌ **Error**: {result['message']}\n\n💡 {result['suggestion']}"
+
+ md = []
+ md.append(f"# FEA Model Analysis\n")
+ md.append(f"**File**: `{result['sim_file']}`\n")
+
+ # Solutions
+ md.append(f"## Solutions ({result['summary']['solution_count']})\n")
+ for sol in result['solutions']:
+ md.append(f"- **{sol['name']}** ({sol['type']}) - Solver: {sol['solver']}")
+ if sol['description']:
+ md.append(f" - {sol['description']}")
+ md.append("")
+
+ # Expressions (Design Variables)
+ md.append(f"## Expressions ({result['summary']['expression_count']})\n")
+ if result['expressions']:
+ md.append("| Name | Value | Units |")
+ md.append("|------|-------|-------|")
+ for expr in result['expressions']:
+ value = expr.get('value', 'N/A')
+ units = expr.get('units', '')
+ md.append(f"| `{expr['name']}` | {value} | {units} |")
+ else:
+ md.append("⚠️ No expressions found. Model may not be parametric.")
+ md.append("")
+
+ # FEM Information
+ fem = result['fem_info']
+ md.append(f"## FEM Information\n")
+
+ if fem['mesh']:
+ md.append(f"**Mesh**: {fem['mesh'].get('name', 'Unknown')}")
+ md.append(f"- Nodes: {fem['mesh'].get('node_count', 'Unknown')}")
+ md.append(f"- Elements: {fem['mesh'].get('element_count', 'Unknown')}")
+ md.append("")
+
+ if fem['materials']:
+ md.append(f"**Materials** ({len(fem['materials'])})")
+ for mat in fem['materials']:
+ md.append(f"- {mat['name']} ({mat['type']})")
+ md.append("")
+
+ if fem['loads']:
+ md.append(f"**Loads** ({len(fem['loads'])})")
+ for load in fem['loads']:
+ md.append(f"- {load['name']} ({load['type']})")
+ md.append("")
+
+ if fem['constraints']:
+ md.append(f"**Constraints** ({len(fem['constraints'])})")
+ for constraint in fem['constraints']:
+ md.append(f"- {constraint['name']} ({constraint['type']})")
+ md.append("")
+
+ # Linked Files
+ if result['linked_files']:
+ md.append(f"## Linked Files\n")
+ for file_type, file_path in result['linked_files'].items():
+ md.append(f"- **{file_type}**: `{file_path}`")
+ md.append("")
+
+ return "\n".join(md)
+
+
+# For testing/debugging
+if __name__ == "__main__":
+ import sys
+
+ if len(sys.argv) < 2:
+ print("Usage: python model_discovery.py ")
+ sys.exit(1)
+
+ sim_path = sys.argv[1]
+ result = discover_fea_model(sim_path)
+
+ if result['status'] == 'success':
+ print(format_discovery_result_for_llm(result))
+ print("\n" + "="*60)
+ print("JSON Output:")
+ print(json.dumps(result, indent=2))
+ else:
+ print(f"Error: {result['message']}")
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', '')}")
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!")
diff --git a/optimization_engine/result_extractors/op2_extractor_example.py b/optimization_engine/result_extractors/op2_extractor_example.py
new file mode 100644
index 00000000..f76df09e
--- /dev/null
+++ b/optimization_engine/result_extractors/op2_extractor_example.py
@@ -0,0 +1,236 @@
+"""
+Example: Result Extraction from OP2 files using pyNastran
+
+This shows how to extract optimization metrics from Nastran OP2 files.
+Common metrics:
+- Max displacement (for stiffness constraints)
+- Max von Mises stress (for strength constraints)
+- Mass (for minimization objectives)
+"""
+
+from pathlib import Path
+from typing import Dict, Any
+import numpy as np
+
+
+def extract_max_displacement(op2_path: Path) -> Dict[str, Any]:
+ """
+ Extract maximum displacement magnitude from OP2 file.
+
+ Args:
+ op2_path: Path to .op2 file
+
+ Returns:
+ Dictionary with max displacement, node ID, and components
+ """
+ from pyNastran.op2.op2 import OP2
+
+ op2 = OP2()
+ op2.read_op2(str(op2_path))
+
+ # Get first subcase (usually the only one in static analysis)
+ subcase_id = list(op2.displacements.keys())[0]
+ displacements = op2.displacements[subcase_id]
+
+ # Extract node IDs and displacement data
+ node_ids = displacements.node_gridtype[:, 0].astype(int)
+ disp_data = displacements.data[0] # First (and usually only) timestep
+
+ # Calculate magnitude: sqrt(dx^2 + dy^2 + dz^2)
+ dx = disp_data[:, 0]
+ dy = disp_data[:, 1]
+ dz = disp_data[:, 2]
+ magnitudes = np.sqrt(dx**2 + dy**2 + dz**2)
+
+ # Find max
+ max_idx = np.argmax(magnitudes)
+ max_displacement = magnitudes[max_idx]
+ max_node_id = node_ids[max_idx]
+
+ return {
+ 'max_displacement': float(max_displacement),
+ 'max_node_id': int(max_node_id),
+ 'dx': float(dx[max_idx]),
+ 'dy': float(dy[max_idx]),
+ 'dz': float(dz[max_idx]),
+ 'units': 'mm', # NX typically uses mm
+ 'subcase': subcase_id
+ }
+
+
+def extract_max_stress(op2_path: Path, stress_type: str = 'von_mises') -> Dict[str, Any]:
+ """
+ Extract maximum stress from OP2 file.
+
+ Args:
+ op2_path: Path to .op2 file
+ stress_type: 'von_mises' or 'max_principal'
+
+ Returns:
+ Dictionary with max stress, element ID, and location
+ """
+ from pyNastran.op2.op2 import OP2
+
+ op2 = OP2()
+ op2.read_op2(str(op2_path))
+
+ # Stress can be in different tables depending on element type
+ # Common: cquad4_stress, ctria3_stress, ctetra_stress, etc.
+ stress_tables = [
+ 'cquad4_stress',
+ 'ctria3_stress',
+ 'ctetra_stress',
+ 'chexa_stress',
+ 'cbar_stress',
+ 'cbeam_stress'
+ ]
+
+ max_stress_overall = 0.0
+ max_element_id = None
+ max_element_type = None
+
+ for table_name in stress_tables:
+ if hasattr(op2, table_name):
+ stress_table = getattr(op2, table_name)
+ if stress_table:
+ subcase_id = list(stress_table.keys())[0]
+ stress_data = stress_table[subcase_id]
+
+ # Extract von Mises stress
+ # Note: Structure varies by element type
+ element_ids = stress_data.element_node[:, 0].astype(int)
+
+ if stress_type == 'von_mises':
+ # von Mises is usually last column
+ stresses = stress_data.data[0, :, -1] # timestep 0, all elements, last column
+ else:
+ # Max principal stress (second-to-last column typically)
+ stresses = stress_data.data[0, :, -2]
+
+ max_stress_in_table = np.max(stresses)
+ if max_stress_in_table > max_stress_overall:
+ max_stress_overall = max_stress_in_table
+ max_idx = np.argmax(stresses)
+ max_element_id = element_ids[max_idx]
+ max_element_type = table_name.replace('_stress', '')
+
+ return {
+ 'max_stress': float(max_stress_overall),
+ 'stress_type': stress_type,
+ 'element_id': int(max_element_id) if max_element_id else None,
+ 'element_type': max_element_type,
+ 'units': 'MPa', # NX typically uses MPa
+ }
+
+
+def extract_mass(op2_path: Path) -> Dict[str, Any]:
+ """
+ Extract total mass from OP2 file.
+
+ Args:
+ op2_path: Path to .op2 file
+
+ Returns:
+ Dictionary with mass and center of gravity
+ """
+ from pyNastran.op2.op2 import OP2
+
+ op2 = OP2()
+ op2.read_op2(str(op2_path))
+
+ # Mass is in grid_point_weight table
+ if hasattr(op2, 'grid_point_weight') and op2.grid_point_weight:
+ mass_data = op2.grid_point_weight
+
+ # Total mass
+ total_mass = mass_data.mass.sum()
+
+ # Center of gravity
+ cg = mass_data.cg
+
+ return {
+ 'total_mass': float(total_mass),
+ 'cg_x': float(cg[0]),
+ 'cg_y': float(cg[1]),
+ 'cg_z': float(cg[2]),
+ 'units': 'kg'
+ }
+ else:
+ # Fallback: Mass not directly available
+ return {
+ 'total_mass': None,
+ 'note': 'Mass data not found in OP2 file. Ensure PARAM,GRDPNT,0 is in Nastran deck'
+ }
+
+
+# Combined extraction function for optimization
+def extract_all_results(op2_path: Path) -> Dict[str, Any]:
+ """
+ Extract all common optimization metrics from OP2 file.
+
+ Args:
+ op2_path: Path to .op2 file
+
+ Returns:
+ Dictionary with all results
+ """
+ results = {
+ 'op2_file': str(op2_path),
+ 'status': 'success'
+ }
+
+ try:
+ results['displacement'] = extract_max_displacement(op2_path)
+ except Exception as e:
+ results['displacement'] = {'error': str(e)}
+
+ try:
+ results['stress'] = extract_max_stress(op2_path)
+ except Exception as e:
+ results['stress'] = {'error': str(e)}
+
+ try:
+ results['mass'] = extract_mass(op2_path)
+ except Exception as e:
+ results['mass'] = {'error': str(e)}
+
+ return results
+
+
+# Example usage
+if __name__ == "__main__":
+ import sys
+ import json
+
+ if len(sys.argv) < 2:
+ print("Usage: python op2_extractor_example.py ")
+ sys.exit(1)
+
+ op2_path = Path(sys.argv[1])
+
+ if not op2_path.exists():
+ print(f"Error: File not found: {op2_path}")
+ sys.exit(1)
+
+ print(f"Extracting results from: {op2_path}")
+ print("=" * 60)
+
+ results = extract_all_results(op2_path)
+
+ print("\nResults:")
+ print(json.dumps(results, indent=2))
+
+ # Summary
+ print("\n" + "=" * 60)
+ print("SUMMARY:")
+ if 'displacement' in results and 'max_displacement' in results['displacement']:
+ disp = results['displacement']
+ print(f" Max Displacement: {disp['max_displacement']:.6f} {disp['units']} at node {disp['max_node_id']}")
+
+ if 'stress' in results and 'max_stress' in results['stress']:
+ stress = results['stress']
+ print(f" Max {stress['stress_type']}: {stress['max_stress']:.2f} {stress['units']} in element {stress['element_id']}")
+
+ if 'mass' in results and 'total_mass' in results['mass'] and results['mass']['total_mass']:
+ mass = results['mass']
+ print(f" Total Mass: {mass['total_mass']:.6f} {mass['units']}")
diff --git a/tests/Bracket.prt b/tests/Bracket.prt
new file mode 100644
index 00000000..82091dc6
Binary files /dev/null and b/tests/Bracket.prt differ
diff --git a/tests/Bracket_fem1.fem b/tests/Bracket_fem1.fem
new file mode 100644
index 00000000..388e4b34
Binary files /dev/null and b/tests/Bracket_fem1.fem differ
diff --git a/tests/Bracket_sim1.sim b/tests/Bracket_sim1.sim
new file mode 100644
index 00000000..2635545f
Binary files /dev/null and b/tests/Bracket_sim1.sim differ
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/bracket_sim1-solution_1.dat b/tests/bracket_sim1-solution_1.dat
new file mode 100644
index 00000000..292aa9fc
--- /dev/null
+++ b/tests/bracket_sim1-solution_1.dat
@@ -0,0 +1,1970 @@
+$*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+$*
+$* Simcenter v2412.0.0.7002 Translator
+$* for Simcenter Nastran version 2412.0
+$*
+$* FEM FILE: C:\Users\antoi\Documents\Atomaste\Atomizer\tests\Bracket_fem1.fem
+$* SIM FILE: C:\Users\antoi\Documents\Atomaste\Atomizer\tests\Bracket_sim1.sim
+$* ANALYSIS TYPE: Structural
+$* SOLUTION NAME: Solution 1
+$* SOLUTION TYPE: SOL 101 Linear Statics
+$*
+$* SOLVER INPUT FILE: bracket_sim1-solution_1.dat
+$* CREATION DATE: 15-Nov-2025
+$* CREATION TIME: 08:33:00
+$* HOSTNAME: AntoineThinkpad
+$* NASTRAN LICENSE: Desktop Bundle
+$*
+$* UNITS: mm (milli-newton)
+$* ... LENGTH : mm
+$* ... TIME : sec
+$* ... MASS : kilogram (kg)
+$* ... TEMPERATURE : deg Celsius
+$* ... FORCE : milli-newton
+$* ... THERMAL ENERGY : mN-mm (micro-joule)
+$*
+$* IMPORTANT NOTE:
+$* This banner was generated by Simcenter and altering this
+$* information may compromise the pre and post processing of results
+$*
+$*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+$*
+$*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+$*
+$* FILE MANAGEMENT
+$*
+$*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+$*
+$*
+$*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+$*
+$* EXECUTIVE CONTROL
+$*
+$*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+$*
+ID,NASTRAN,bracket_sim1-solution_1
+SOL 101
+CEND
+$*
+$*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+$*
+$* CASE CONTROL
+$*
+$*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+$*
+ECHO = NONE
+OUTPUT
+DISPLACEMENT(PLOT,REAL) = ALL
+SPCFORCES(PLOT,REAL) = ALL
+STRESS(PLOT,REAL,VONMISES,CENTER) = ALL
+$* Step: Subcase - Statics 1
+SUBCASE 1
+ LABEL = Subcase - Statics 1
+ LOAD = 1
+ SPC = 2
+$*
+$*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+$*
+$* BULK DATA
+$*
+$*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+$*
+BEGIN BULK
+$*
+$* PARAM CARDS
+$*
+PARAM K6ROT100.0000
+PARAM OIBULK YES
+PARAM OMACHPR YES
+PARAM POST -2
+PARAM POSTEXT YES
+PARAM UNITSYS MN-MM
+$*
+$* GRID CARDS
+$*
+GRID* 1 0-5.000000000E+003.0809149719E-14+
+* 6.0969398499E+01 0
+GRID* 2 0-5.000000000E+000.0000000000E+00+
+* 0.0000000000E+00 0
+GRID* 3 0-5.000000000E+001.0000000000E+01+
+* 0.0000000000E+00 0
+GRID* 4 0-5.000000000E+003.0766941071E+01+
+* 3.5969398499E+01 0
+GRID* 5 0-5.000000000E+003.9427196503E+01+
+* 4.0969398499E+01 0
+GRID* 6 0-5.000000000E+001.0000000000E+02+
+* 6.0969398499E+01 0
+GRID* 7 0-5.000000000E+001.0000000000E+02+
+* 4.0969398499E+01 0
+GRID* 8 05.0000000000E+000.0000000000E+00+
+* 0.0000000000E+00 0
+GRID* 9 05.0000000000E+003.0809149719E-14+
+* 6.0969398499E+01 0
+GRID* 10 05.0000000000E+001.0000000000E+01+
+* 0.0000000000E+00 0
+GRID* 11 05.0000000000E+003.0766941071E+01+
+* 3.5969398499E+01 0
+GRID* 12 05.0000000000E+003.9427196503E+01+
+* 4.0969398499E+01 0
+GRID* 13 05.0000000000E+001.0000000000E+02+
+* 4.0969398499E+01 0
+GRID* 14 05.0000000000E+001.0000000000E+02+
+* 6.0969398499E+01 0
+GRID* 15 0-5.000000000E+005.6599138936E-15+
+* 1.1200618933E+01 0
+GRID* 16 0-5.000000000E+007.4463678094E-15+
+* 1.4735900552E+01 0
+GRID* 17 0-5.000000000E+001.2998593569E-14+
+* 2.5723411339E+01 0
+GRID* 18 0-5.000000000E+001.1146461935E-14+
+* 2.2058157584E+01 0
+GRID* 19 0-5.000000000E+001.4878673103E-14+
+* 2.9443972255E+01 0
+GRID* 20 0-5.000000000E+001.6776288650E-14+
+* 3.3199235856E+01 0
+GRID* 21 0-5.000000000E+009.2818773938E-15+
+* 1.8368260299E+01 0
+GRID* 22 0-5.000000000E+002.4509094810E-14+
+* 4.8501980156E+01 0
+GRID* 23 0-5.000000000E+002.2516325714E-14+
+* 4.4558413578E+01 0
+GRID* 24 0-5.000000000E+001.8657565464E-14+
+* 3.6922166114E+01 0
+GRID* 25 0-5.000000000E+002.6552877281E-14+
+* 5.2546499042E+01 0
+GRID* 26 0-5.000000000E+002.8654609257E-14+
+* 5.6705696408E+01 0
+GRID* 27 0-5.000000000E+002.0564375719E-14+
+* 4.0695625473E+01 0
+GRID* 28 0-5.000000000E+003.8249043630E-15+
+* 7.5692487605E+00 0
+GRID* 29 0-5.000000000E+001.9481352367E-15+
+* 3.8552389357E+00 0
+GRID* 30 0-5.000000000E+003.5024129445E+00+
+* 0.0000000000E+00 0
+GRID* 31 0-5.000000000E+007.0841633353E+00+
+* 0.0000000000E+00 0
+GRID* 32 0-5.000000000E+002.9304451981E+01+
+* 3.3436292988E+01 0
+GRID* 33 0-5.000000000E+002.7390009892E+01+
+* 3.0120381888E+01 0
+GRID* 34 0-5.000000000E+002.5408015081E+01+
+* 2.6687466038E+01 0
+GRID* 35 0-5.000000000E+002.1555376243E+01+
+* 2.0014499559E+01 0
+GRID* 36 0-5.000000000E+002.3466452750E+01+
+* 2.3324581298E+01 0
+GRID* 37 0-5.000000000E+001.9699822681E+01+
+* 1.6800586383E+01 0
+GRID* 38 0-5.000000000E+001.6069470407E+01+
+* 1.0512631543E+01 0
+GRID* 39 0-5.000000000E+001.7856460381E+01+
+* 1.3607789095E+01 0
+GRID* 40 0-5.000000000E+001.4376422902E+01+
+* 7.5801871258E+00 0
+GRID* 41 0-5.000000000E+001.2730156969E+01+
+* 4.7287707737E+00 0
+GRID* 42 0-5.000000000E+001.1243918277E+01+
+* 2.1545297429E+00 0
+GRID* 43 0-5.000000000E+003.6136957970E+01+
+* 4.0357355675E+01 0
+GRID* 44 0-5.000000000E+003.3533547646E+01+
+* 3.9045344197E+01 0
+GRID* 45 0-5.000000000E+003.0288176981E+01+
+* 6.0969398499E+01 0
+GRID* 46 0-5.000000000E+002.6219783709E+01+
+* 6.0969398499E+01 0
+GRID* 47 0-5.000000000E+003.4395022243E+01+
+* 6.0969398499E+01 0
+GRID* 48 0-5.000000000E+004.2802664765E+01+
+* 6.0969398499E+01 0
+GRID* 49 0-5.000000000E+004.6988428548E+01+
+* 6.0969398499E+01 0
+GRID* 50 0-5.000000000E+003.8587374217E+01+
+* 6.0969398499E+01 0
+GRID* 51 0-5.000000000E+006.7510060672E+01+
+* 6.0969398499E+01 0
+GRID* 52 0-5.000000000E+006.3455753671E+01+
+* 6.0969398499E+01 0
+GRID* 53 0-5.000000000E+005.9370054951E+01+
+* 6.0969398499E+01 0
+GRID* 54 0-5.000000000E+005.1137500769E+01+
+* 6.0969398499E+01 0
+GRID* 55 0-5.000000000E+005.5262296559E+01+
+* 6.0969398499E+01 0
+GRID* 56 0-5.000000000E+007.1572616289E+01+
+* 6.0969398499E+01 0
+GRID* 57 0-5.000000000E+007.5636537064E+01+
+* 6.0969398499E+01 0
+GRID* 58 0-5.000000000E+007.9714010810E+01+
+* 6.0969398499E+01 0
+GRID* 59 0-5.000000000E+008.3793617603E+01+
+* 6.0969398499E+01 0
+GRID* 60 0-5.000000000E+009.1932211807E+01+
+* 6.0969398499E+01 0
+GRID* 61 0-5.000000000E+009.5968857727E+01+
+* 6.0969398499E+01 0
+GRID* 62 0-5.000000000E+008.8577358654E+00+
+* 6.0969398499E+01 0
+GRID* 63 0-5.000000000E+004.4264576473E+00+
+* 6.0969398499E+01 0
+GRID* 64 0-5.000000000E+008.7869704397E+01+
+* 6.0969398499E+01 0
+GRID* 65 0-5.000000000E+001.3292095420E+01+
+* 6.0969398499E+01 0
+GRID* 66 0-5.000000000E+001.7702276246E+01+
+* 6.0969398499E+01 0
+GRID* 67 0-5.000000000E+002.2030172574E+01+
+* 6.0969398499E+01 0
+GRID* 68 0-5.000000000E+005.9392142471E+01+
+* 4.0969398499E+01 0
+GRID* 69 0-5.000000000E+006.3467673597E+01+
+* 4.0969398499E+01 0
+GRID* 70 0-5.000000000E+005.1226024617E+01+
+* 4.0969398499E+01 0
+GRID* 71 0-5.000000000E+005.5307029501E+01+
+* 4.0969398499E+01 0
+GRID* 72 0-5.000000000E+004.7164089390E+01+
+* 4.0969398499E+01 0
+GRID* 73 0-5.000000000E+004.3181930500E+01+
+* 4.0969398499E+01 0
+GRID* 74 0-5.000000000E+006.7516125932E+01+
+* 4.0969398499E+01 0
+GRID* 75 0-5.000000000E+009.5968337974E+01+
+* 4.0969398499E+01 0
+GRID* 76 0-5.000000000E+009.1932157327E+01+
+* 4.0969398499E+01 0
+GRID* 77 0-5.000000000E+008.7869946355E+01+
+* 4.0969398499E+01 0
+GRID* 78 0-5.000000000E+007.9714807161E+01+
+* 4.0969398499E+01 0
+GRID* 79 0-5.000000000E+007.5638061444E+01+
+* 4.0969398499E+01 0
+GRID* 80 0-5.000000000E+007.1575575698E+01+
+* 4.0969398499E+01 0
+GRID* 81 0-5.000000000E+008.3793963486E+01+
+* 4.0969398499E+01 0
+GRID* 82 0-5.000000000E+001.0000000000E+02+
+* 4.6014875615E+01 0
+GRID* 83 0-5.000000000E+001.0000000000E+02+
+* 5.0977922286E+01 0
+GRID* 84 0-5.000000000E+001.0000000000E+02+
+* 5.5931393500E+01 0
+GRID* 85 05.0149807778E-030.0000000000E+00+
+* 0.0000000000E+00 0
+GRID* 86 05.2099017236E-033.0809149719E-14+
+* 6.0969398499E+01 0
+GRID* 87 05.1094438118E-031.0000000000E+01+
+* 0.0000000000E+00 0
+GRID* 88 05.4978155655E-033.0766941071E+01+
+* 3.5969398499E+01 0
+GRID* 89 05.6290425668E-033.9427196503E+01+
+* 4.0969398499E+01 0
+GRID* 90 05.3952402233E-031.0000000000E+02+
+* 4.0969398499E+01 0
+GRID* 91 05.2102686847E-031.0000000000E+02+
+* 6.0969398499E+01 0
+GRID* 92 05.0000000000E+003.8251026909E-15+
+* 7.5696412391E+00 0
+GRID* 93 05.0000000000E+001.9485099981E-15+
+* 3.8559805653E+00 0
+GRID* 94 05.0000000000E+001.1146499179E-14+
+* 2.2058231288E+01 0
+GRID* 95 05.0000000000E+001.2998622030E-14+
+* 2.5723467662E+01 0
+GRID* 96 05.0000000000E+001.4878702140E-14+
+* 2.9444029718E+01 0
+GRID* 97 05.0000000000E+001.6776311170E-14+
+* 3.3199280421E+01 0
+GRID* 98 05.0000000000E+009.2819237749E-15+
+* 1.8368352084E+01 0
+GRID* 99 05.0000000000E+007.4464265072E-15+
+* 1.4736016711E+01 0
+GRID* 100 05.0000000000E+002.8654612998E-14+
+* 5.6705703813E+01 0
+GRID* 101 05.0000000000E+002.6552884026E-14+
+* 5.2546512391E+01 0
+GRID* 102 05.0000000000E+002.4509105680E-14+
+* 4.8502001668E+01 0
+GRID* 103 05.0000000000E+002.2516338299E-14+
+* 4.4558438484E+01 0
+GRID* 104 05.0000000000E+002.0564394189E-14+
+* 4.0695662023E+01 0
+GRID* 105 05.0000000000E+001.8657587566E-14+
+* 3.6922209852E+01 0
+GRID* 106 05.0000000000E+005.6600020874E-15+
+* 1.1200793463E+01 0
+GRID* 107 05.0000000000E+007.0829159424E+00+
+* 0.0000000000E+00 0
+GRID* 108 05.0000000000E+003.5020416032E+00+
+* 0.0000000000E+00 0
+GRID* 109 05.0000000000E+001.1243279170E+01+
+* 2.1534227763E+00 0
+GRID* 110 05.0000000000E+002.9304095846E+01+
+* 3.3435676144E+01 0
+GRID* 111 05.0000000000E+001.6069447535E+01+
+* 1.0512591928E+01 0
+GRID* 112 05.0000000000E+001.7856248355E+01+
+* 1.3607421854E+01 0
+GRID* 113 05.0000000000E+002.7389943247E+01+
+* 3.0120266456E+01 0
+GRID* 114 05.0000000000E+002.5407952489E+01+
+* 2.6687357624E+01 0
+GRID* 115 05.0000000000E+001.9699724948E+01+
+* 1.6800417106E+01 0
+GRID* 116 05.0000000000E+002.1555389278E+01+
+* 2.0014522135E+01 0
+GRID* 117 05.0000000000E+002.3466535396E+01+
+* 2.3324724446E+01 0
+GRID* 118 05.0000000000E+001.4376418155E+01+
+* 7.5801789052E+00 0
+GRID* 119 05.0000000000E+001.2730044924E+01+
+* 4.7285767050E+00 0
+GRID* 120 05.0000000000E+003.3533540603E+01+
+* 3.9045337856E+01 0
+GRID* 121 05.0000000000E+003.6137163716E+01+
+* 4.0357422526E+01 0
+GRID* 122 05.0000000000E+003.0288181507E+01+
+* 6.0969398499E+01 0
+GRID* 123 05.0000000000E+003.4395017111E+01+
+* 6.0969398499E+01 0
+GRID* 124 05.0000000000E+002.6219812464E+01+
+* 6.0969398499E+01 0
+GRID* 125 05.0000000000E+003.8587367319E+01+
+* 6.0969398499E+01 0
+GRID* 126 05.0000000000E+004.2802652841E+01+
+* 6.0969398499E+01 0
+GRID* 127 05.0000000000E+006.3455757541E+01+
+* 6.0969398499E+01 0
+GRID* 128 05.0000000000E+006.7510065076E+01+
+* 6.0969398499E+01 0
+GRID* 129 05.0000000000E+005.9370055353E+01+
+* 6.0969398499E+01 0
+GRID* 130 05.0000000000E+005.5262293984E+01+
+* 6.0969398499E+01 0
+GRID* 131 05.0000000000E+005.1137495423E+01+
+* 6.0969398499E+01 0
+GRID* 132 05.0000000000E+004.6988427828E+01+
+* 6.0969398499E+01 0
+GRID* 133 05.0000000000E+007.5636537417E+01+
+* 6.0969398499E+01 0
+GRID* 134 05.0000000000E+007.1572619625E+01+
+* 6.0969398499E+01 0
+GRID* 135 05.0000000000E+007.9714010552E+01+
+* 6.0969398499E+01 0
+GRID* 136 05.0000000000E+008.3793616445E+01+
+* 6.0969398499E+01 0
+GRID* 137 05.0000000000E+008.7869701402E+01+
+* 6.0969398499E+01 0
+GRID* 138 05.0000000000E+009.1932210970E+01+
+* 6.0969398499E+01 0
+GRID* 139 05.0000000000E+009.5968853910E+01+
+* 6.0969398499E+01 0
+GRID* 140 05.0000000000E+004.4264627718E+00+
+* 6.0969398499E+01 0
+GRID* 141 05.0000000000E+008.8577525524E+00+
+* 6.0969398499E+01 0
+GRID* 142 05.0000000000E+001.7702295276E+01+
+* 6.0969398499E+01 0
+GRID* 143 05.0000000000E+002.2030196659E+01+
+* 6.0969398499E+01 0
+GRID* 144 05.0000000000E+001.3292113057E+01+
+* 6.0969398499E+01 0
+GRID* 145 05.0000000000E+005.9392147692E+01+
+* 4.0969398499E+01 0
+GRID* 146 05.0000000000E+006.3467674794E+01+
+* 4.0969398499E+01 0
+GRID* 147 05.0000000000E+005.5307033645E+01+
+* 4.0969398499E+01 0
+GRID* 148 05.0000000000E+006.7516124144E+01+
+* 4.0969398499E+01 0
+GRID* 149 05.0000000000E+005.1226027554E+01+
+* 4.0969398499E+01 0
+GRID* 150 05.0000000000E+004.7164117708E+01+
+* 4.0969398499E+01 0
+GRID* 151 05.0000000000E+004.3181977028E+01+
+* 4.0969398499E+01 0
+GRID* 152 05.0000000000E+009.5968333205E+01+
+* 4.0969398499E+01 0
+GRID* 153 05.0000000000E+009.1932158349E+01+
+* 4.0969398499E+01 0
+GRID* 154 05.0000000000E+008.3793964011E+01+
+* 4.0969398499E+01 0
+GRID* 155 05.0000000000E+008.7869944682E+01+
+* 4.0969398499E+01 0
+GRID* 156 05.0000000000E+007.9714807966E+01+
+* 4.0969398499E+01 0
+GRID* 157 05.0000000000E+007.5638067018E+01+
+* 4.0969398499E+01 0
+GRID* 158 05.0000000000E+007.1575585475E+01+
+* 4.0969398499E+01 0
+GRID* 159 05.0000000000E+001.0000000000E+02+
+* 4.6014844030E+01 0
+GRID* 160 05.0000000000E+001.0000000000E+02+
+* 5.5931419254E+01 0
+GRID* 161 05.0000000000E+001.0000000000E+02+
+* 5.0977923066E+01 0
+GRID* 162 0-5.000000000E+003.8365121664E+00+
+* 7.4284499561E+00 0
+GRID* 163 0-5.000000000E+008.3019736448E+00+
+* 6.6599196629E+00 0
+GRID* 164 0-5.000000000E+003.5858243885E+00+
+* 3.7864582063E+00 0
+GRID* 165 0-5.000000000E+007.4111186332E+00+
+* 3.3924111896E+00 0
+GRID* 166 0-5.000000000E+003.0261966325E+01+
+* 5.5797624131E+01 0
+GRID* 167 0-5.000000000E+002.6202825136E+01+
+* 5.5900797863E+01 0
+GRID* 168 0-5.000000000E+002.6046207614E+01+
+* 5.0799626429E+01 0
+GRID* 169 0-5.000000000E+003.0206371270E+01+
+* 5.0587846507E+01 0
+GRID* 170 0-5.000000000E+003.4425731238E+01+
+* 5.0638502354E+01 0
+GRID* 171 0-5.000000000E+003.4382597204E+01+
+* 5.5796642686E+01 0
+GRID* 172 0-5.000000000E+003.8738469389E+01+
+* 5.0814645657E+01 0
+GRID* 173 0-5.000000000E+003.8606803936E+01+
+* 5.5841311807E+01 0
+GRID* 174 0-5.000000000E+004.2935806391E+01+
+* 5.0930227602E+01 0
+GRID* 175 0-5.000000000E+004.7064561723E+01+
+* 5.0980110334E+01 0
+GRID* 176 0-5.000000000E+004.7008968620E+01+
+* 5.5922357733E+01 0
+GRID* 177 0-5.000000000E+004.2835144637E+01+
+* 5.5890902902E+01 0
+GRID* 178 0-5.000000000E+003.9717010439E+00+
+* 1.1028442634E+01 0
+GRID* 179 0-5.000000000E+003.9729639623E+00+
+* 1.4572077720E+01 0
+GRID* 180 0-5.000000000E+005.9388994694E+01+
+* 4.6035564386E+01 0
+GRID* 181 0-5.000000000E+006.3465851338E+01+
+* 4.6031046831E+01 0
+GRID* 182 0-5.000000000E+005.1211153059E+01+
+* 4.6041675710E+01 0
+GRID* 183 0-5.000000000E+005.5299839154E+01+
+* 4.6039357388E+01 0
+GRID* 184 0-5.000000000E+006.7513021829E+01+
+* 5.0989248017E+01 0
+GRID* 185 0-5.000000000E+006.7515157582E+01+
+* 4.6026854479E+01 0
+GRID* 186 0-5.000000000E+006.3461604090E+01+
+* 5.0993508791E+01 0
+GRID* 187 0-5.000000000E+006.3457447083E+01+
+* 5.5940079475E+01 0
+GRID* 188 0-5.000000000E+006.7510905387E+01+
+* 5.5937681938E+01 0
+GRID* 189 0-5.000000000E+005.9373067231E+01+
+* 5.5942003156E+01 0
+GRID* 190 0-5.000000000E+004.5944108102E+00+
+* 2.5533999190E+01 0
+GRID* 191 0-5.000000000E+009.2507377041E+00+
+* 2.4835122196E+01 0
+GRID* 192 0-5.000000000E+009.8438147377E+00+
+* 2.8549810729E+01 0
+GRID* 193 0-5.000000000E+004.8011338595E+00+
+* 2.9262711243E+01 0
+GRID* 194 0-5.000000000E+005.9380921484E+01+
+* 5.0997772036E+01 0
+GRID* 195 0-5.000000000E+005.1178301095E+01+
+* 5.0996946556E+01 0
+GRID* 196 0-5.000000000E+005.5283703372E+01+
+* 5.1000045689E+01 0
+GRID* 197 0-5.000000000E+005.1149252786E+01+
+* 5.5937387372E+01 0
+GRID* 198 0-5.000000000E+005.5268523157E+01+
+* 5.5942200447E+01 0
+GRID* 199 0-5.000000000E+004.3097432551E+01+
+* 4.6000435376E+01 0
+GRID* 200 0-5.000000000E+004.7134712784E+01+
+* 4.6037037515E+01 0
+GRID* 201 0-5.000000000E+003.0056084717E+01+
+* 4.4465066164E+01 0
+GRID* 202 0-5.000000000E+003.4795603625E+01+
+* 4.5350245837E+01 0
+GRID* 203 0-5.000000000E+003.9065137652E+01+
+* 4.5841725194E+01 0
+GRID* 204 0-5.000000000E+002.7445280290E+01+
+* 3.9447357212E+01 0
+GRID* 205 0-5.000000000E+002.5130745611E+01+
+* 3.5814551797E+01 0
+GRID* 206 0-5.000000000E+004.3067709387E+00+
+* 2.1899179071E+01 0
+GRID* 207 0-5.000000000E+004.5854119095E+00+
+* 3.3052149312E+01 0
+GRID* 208 0-5.000000000E+001.0408676214E+01+
+* 3.2314503766E+01 0
+GRID* 209 0-5.000000000E+004.0281942222E+00+
+* 1.8212479069E+01 0
+GRID* 210 0-5.000000000E+002.5539380545E+01+
+* 4.5550113508E+01 0
+GRID* 211 0-5.000000000E+002.1377788207E+01+
+* 4.6561264295E+01 0
+GRID* 212 0-5.000000000E+002.1815807140E+01+
+* 5.1233535845E+01 0
+GRID* 213 0-5.000000000E+008.5491014793E+00+
+* 2.1210936626E+01 0
+GRID* 214 0-5.000000000E+007.7954383411E+00+
+* 1.7612877969E+01 0
+GRID* 215 0-5.000000000E+001.0381218799E+01+
+* 1.3190337870E+01 0
+GRID* 216 0-5.000000000E+001.1479821165E+01+
+* 1.6586273715E+01 0
+GRID* 217 0-5.000000000E+006.9404112616E+00+
+* 1.4094311450E+01 0
+GRID* 218 0-5.000000000E+004.1909105512E+00+
+* 4.4483623783E+01 0
+GRID* 219 0-5.000000000E+004.2970293940E+00+
+* 4.8453263108E+01 0
+GRID* 220 0-5.000000000E+001.3751268708E+01+
+* 3.1552983633E+01 0
+GRID* 221 0-5.000000000E+001.3958841105E+01+
+* 2.3572553427E+01 0
+GRID* 222 0-5.000000000E+001.5486324956E+01+
+* 2.7041435400E+01 0
+GRID* 223 0-5.000000000E+004.0579883040E+00+
+* 3.6836491960E+01 0
+GRID* 224 0-5.000000000E+009.1932166209E+01+
+* 4.6015631759E+01 0
+GRID* 225 0-5.000000000E+009.5968305069E+01+
+* 4.6014980640E+01 0
+GRID* 226 0-5.000000000E+002.3276271916E+01+
+* 3.2305606592E+01 0
+GRID* 227 0-5.000000000E+008.7869905730E+01+
+* 4.6016563319E+01 0
+GRID* 228 0-5.000000000E+002.1414972922E+01+
+* 2.8827733176E+01 0
+GRID* 229 0-5.000000000E+007.1574102419E+01+
+* 5.0987859150E+01 0
+GRID* 230 0-5.000000000E+007.1573010223E+01+
+* 5.5936801339E+01 0
+GRID* 231 0-5.000000000E+007.5636781770E+01+
+* 5.5934522831E+01 0
+GRID* 232 0-5.000000000E+007.5637327480E+01+
+* 5.0984524027E+01 0
+GRID* 233 0-5.000000000E+007.9714096504E+01+
+* 5.5932797395E+01 0
+GRID* 234 0-5.000000000E+007.9714391488E+01+
+* 5.0982003384E+01 0
+GRID* 235 0-5.000000000E+001.9680545589E+01+
+* 2.5364118146E+01 0
+GRID* 236 0-5.000000000E+001.7987636184E+01+
+* 2.1934866221E+01 0
+GRID* 237 0-5.000000000E+008.3793933627E+01+
+* 4.6017729484E+01 0
+GRID* 238 0-5.000000000E+008.7869831344E+01+
+* 5.0978719974E+01 0
+GRID* 239 0-5.000000000E+008.3793801201E+01+
+* 5.0980100253E+01 0
+GRID* 240 0-5.000000000E+008.7869740953E+01+
+* 5.5930957551E+01 0
+GRID* 241 0-5.000000000E+008.3793682491E+01+
+* 5.5931582326E+01 0
+GRID* 242 0-5.000000000E+009.5968347748E+01+
+* 5.0977911013E+01 0
+GRID* 243 0-5.000000000E+009.5968551832E+01+
+* 5.5931268958E+01 0
+GRID* 244 0-5.000000000E+009.1932201614E+01+
+* 5.5930890555E+01 0
+GRID* 245 0-5.000000000E+009.1932183077E+01+
+* 5.0978024842E+01 0
+GRID* 246 0-5.000000000E+001.7194804782E+01+
+* 3.0539057589E+01 0
+GRID* 247 0-5.000000000E+001.8603797858E+01+
+* 3.4176464665E+01 0
+GRID* 248 0-5.000000000E+001.2697834455E+01+
+* 2.0062966605E+01 0
+GRID* 249 0-5.000000000E+001.6317123075E+01+
+* 1.8592003704E+01 0
+GRID* 250 0-5.000000000E+007.9714675653E+01+
+* 4.6019460461E+01 0
+GRID* 251 0-5.000000000E+007.5637874859E+01+
+* 4.6021807753E+01 0
+GRID* 252 0-5.000000000E+007.1575198131E+01+
+* 4.6025363409E+01 0
+GRID* 253 0-5.000000000E+004.3646207187E+00+
+* 5.2493734364E+01 0
+GRID* 254 0-5.000000000E+008.5311567074E+00+
+* 4.8241101060E+01 0
+GRID* 255 0-5.000000000E+008.7261968294E+00+
+* 5.2343566168E+01 0
+GRID* 256 0-5.000000000E+008.8357886711E+00+
+* 5.6606495042E+01 0
+GRID* 257 0-5.000000000E+004.4201932880E+00+
+* 5.6687422057E+01 0
+GRID* 258 0-5.000000000E+002.4240937228E+01+
+* 4.0958902174E+01 0
+GRID* 259 0-5.000000000E+001.9750114465E+01+
+* 3.8006838560E+01 0
+GRID* 260 0-5.000000000E+001.4792199709E+01+
+* 3.5287813104E+01 0
+GRID* 261 0-5.000000000E+002.0639224613E+01+
+* 4.2145096266E+01 0
+GRID* 262 0-5.000000000E+001.5686003112E+01+
+* 3.9101196029E+01 0
+GRID* 263 0-5.000000000E+001.6470867985E+01+
+* 4.3102758818E+01 0
+GRID* 264 0-5.000000000E+001.7068280210E+01+
+* 4.7295961041E+01 0
+GRID* 265 0-5.000000000E+008.2502923495E+00+
+* 4.4242193434E+01 0
+GRID* 266 0-5.000000000E+007.3856354839E+00+
+* 3.6567038836E+01 0
+GRID* 267 0-5.000000000E+001.1080322586E+01+
+* 3.6048222966E+01 0
+GRID* 268 0-5.000000000E+001.4672953953E+01+
+* 1.5287289605E+01 0
+GRID* 269 0-5.000000000E+007.8817572481E+00+
+* 4.0348421218E+01 0
+GRID* 270 0-5.000000000E+001.1754800011E+01+
+* 3.9859681028E+01 0
+GRID* 271 0-5.000000000E+004.1125081512E+00+
+* 4.0620057734E+01 0
+GRID* 272 0-5.000000000E+001.2310648162E+01+
+* 4.3795021939E+01 0
+GRID* 273 0-5.000000000E+009.3781249413E+00+
+* 9.8441626418E+00 0
+GRID* 274 0-5.000000000E+002.2011752999E+01+
+* 5.6083194573E+01 0
+GRID* 275 0-5.000000000E+001.2770197843E+01+
+* 4.7829696614E+01 0
+GRID* 276 0-5.000000000E+001.3083325242E+01+
+* 5.2070351351E+01 0
+GRID* 277 0-5.000000000E+001.7469402021E+01+
+* 5.1689557303E+01 0
+GRID* 278 0-5.000000000E+001.7672726100E+01+
+* 5.6289588292E+01 0
+GRID* 279 0-5.000000000E+001.3256496106E+01+
+* 5.6471574922E+01 0
+GRID* 280 05.2254857519E-035.6597138560E-15+
+* 1.1200223071E+01 0
+GRID* 281 05.2658229352E-037.4461618604E-15+
+* 1.4735492991E+01 0
+GRID* 282 05.1394950470E-033.8247632052E-15+
+* 7.5689694180E+00 0
+GRID* 283 05.0430063071E-031.9479040733E-15+
+* 3.8547814776E+00 0
+GRID* 284 05.3587535366E-031.1146313285E-14+
+* 2.2057863415E+01 0
+GRID* 285 05.3747609166E-031.2998457965E-14+
+* 2.5723142986E+01 0
+GRID* 286 05.4028904636E-031.4878550597E-14+
+* 2.9443729825E+01 0
+GRID* 287 05.4008342812E-031.6776189467E-14+
+* 3.3199039577E+01 0
+GRID* 288 05.3252820565E-039.2817242372E-15+
+* 1.8367957212E+01 0
+GRID* 289 05.4027604829E-032.2516257635E-14+
+* 4.4558278853E+01 0
+GRID* 290 05.3569058093E-032.4509059034E-14+
+* 4.8501909358E+01 0
+GRID* 291 05.4084787353E-031.8657459583E-14+
+* 3.6921956582E+01 0
+GRID* 292 05.2564979044E-032.8654596369E-14+
+* 5.6705670904E+01 0
+GRID* 293 05.3114795296E-032.6552862920E-14+
+* 5.2546470624E+01 0
+GRID* 294 05.4188574359E-032.0564314441E-14+
+* 4.0695504207E+01 0
+GRID* 295 05.0245207723E-033.5024088424E+00+
+* 0.0000000000E+00 0
+GRID* 296 05.0823261880E-037.0877025377E+00+
+* 0.0000000000E+00 0
+GRID* 297 05.1678079559E-031.1242187412E+01+
+* 2.1515317975E+00 0
+GRID* 298 05.4966796687E-032.9306433243E+01+
+* 3.3439724634E+01 0
+GRID* 299 05.3875079762E-031.6069794700E+01+
+* 1.0513193235E+01 0
+GRID* 300 05.4197991684E-031.7857382416E+01+
+* 1.3609386107E+01 0
+GRID* 301 05.5026041388E-032.7377194902E+01+
+* 3.0098185674E+01 0
+GRID* 302 05.4898124908E-032.5408367861E+01+
+* 2.6688077070E+01 0
+GRID* 303 05.4959988386E-032.1572718486E+01+
+* 2.0044537206E+01 0
+GRID* 304 05.4148212568E-031.9699858840E+01+
+* 1.6800649014E+01 0
+GRID* 305 05.5066523055E-032.3469294530E+01+
+* 2.3329503407E+01 0
+GRID* 306 05.3170301457E-031.4377184979E+01+
+* 7.5815070818E+00 0
+GRID* 307 05.2390163530E-031.2731479052E+01+
+* 4.7310606873E+00 0
+GRID* 308 05.5998550378E-033.6137654296E+01+
+* 4.0357581925E+01 0
+GRID* 309 05.5177899784E-033.3533438340E+01+
+* 3.9045245778E+01 0
+GRID* 310 05.4395328609E-033.0288232762E+01+
+* 6.0969398499E+01 0
+GRID* 311 05.4951315654E-032.6219931544E+01+
+* 6.0969398499E+01 0
+GRID* 312 05.4971397712E-033.4394852291E+01+
+* 6.0969398499E+01 0
+GRID* 313 05.5007013505E-033.8587334368E+01+
+* 6.0969398499E+01 0
+GRID* 314 05.4554501964E-034.2802622182E+01+
+* 6.0969398499E+01 0
+GRID* 315 05.4617495710E-034.6988435748E+01+
+* 6.0969398499E+01 0
+GRID* 316 05.5066690895E-036.3455751437E+01+
+* 6.0969398499E+01 0
+GRID* 317 05.5045119972E-036.7510070172E+01+
+* 6.0969398499E+01 0
+GRID* 318 05.5128549463E-035.9370045419E+01+
+* 6.0969398499E+01 0
+GRID* 319 05.4624905952E-035.5262296406E+01+
+* 6.0969398499E+01 0
+GRID* 320 05.4676802500E-035.1137468923E+01+
+* 6.0969398499E+01 0
+GRID* 321 05.4539606750E-037.1572616995E+01+
+* 6.0969398499E+01 0
+GRID* 322 05.4485721924E-037.5636544474E+01+
+* 6.0969398499E+01 0
+GRID* 323 05.4316444717E-037.9714001137E+01+
+* 6.0969398499E+01 0
+GRID* 324 05.4049192368E-038.3793634761E+01+
+* 6.0969398499E+01 0
+GRID* 325 05.3930559039E-038.7869675396E+01+
+* 6.0969398499E+01 0
+GRID* 326 05.2428199584E-039.5968774637E+01+
+* 6.0969398499E+01 0
+GRID* 327 05.3155463860E-039.1932247737E+01+
+* 6.0969398499E+01 0
+GRID* 328 05.2381958383E-034.4264876379E+00+
+* 6.0969398499E+01 0
+GRID* 329 05.3123510316E-038.8578145490E+00+
+* 6.0969398499E+01 0
+GRID* 330 05.4845379400E-032.2030434585E+01+
+* 6.0969398499E+01 0
+GRID* 331 05.4714560238E-031.7702566532E+01+
+* 6.0969398499E+01 0
+GRID* 332 05.3789696702E-031.3292188735E+01+
+* 6.0969398499E+01 0
+GRID* 333 05.7628527603E-035.9392148152E+01+
+* 4.0969398499E+01 0
+GRID* 334 05.7602670734E-036.3467680495E+01+
+* 4.0969398499E+01 0
+GRID* 335 05.7549397954E-035.5307046863E+01+
+* 4.0969398499E+01 0
+GRID* 336 05.7620110452E-036.7516126073E+01+
+* 4.0969398499E+01 0
+GRID* 337 05.7328846864E-035.1226065890E+01+
+* 4.0969398499E+01 0
+GRID* 338 05.7089741275E-034.7164402549E+01+
+* 4.0969398499E+01 0
+GRID* 339 05.6390662531E-034.3182037549E+01+
+* 4.0969398499E+01 0
+GRID* 340 05.5322811782E-039.1932185183E+01+
+* 4.0969398499E+01 0
+GRID* 341 05.4160570910E-039.5968227119E+01+
+* 4.0969398499E+01 0
+GRID* 342 05.6959347775E-038.3793979843E+01+
+* 4.0969398499E+01 0
+GRID* 343 05.6239673368E-038.7869920872E+01+
+* 4.0969398499E+01 0
+GRID* 344 05.7299466726E-037.9714798164E+01+
+* 4.0969398499E+01 0
+GRID* 345 05.7463512599E-037.5638083432E+01+
+* 4.0969398499E+01 0
+GRID* 346 05.7700215971E-037.1575579651E+01+
+* 4.0969398499E+01 0
+GRID* 347 05.3550421035E-031.0000000000E+02+
+* 4.6014888910E+01 0
+GRID* 348 05.3259000957E-031.0000000000E+02+
+* 5.0977906001E+01 0
+GRID* 349 05.2651214887E-031.0000000000E+02+
+* 5.5931422411E+01 0
+GRID* 350 05.0000000000E+007.4113330594E+00+
+* 3.3940645029E+00 0
+GRID* 351 05.0000000000E+004.2835126991E+01+
+* 5.5890944626E+01 0
+GRID* 352 05.0000000000E+004.2935764189E+01+
+* 5.0930242467E+01 0
+GRID* 353 05.0000000000E+004.7008955624E+01+
+* 5.5922400053E+01 0
+GRID* 354 05.0000000000E+004.7064558562E+01+
+* 5.0980119969E+01 0
+GRID* 355 05.0000000000E+003.8366602877E+00+
+* 7.4287152518E+00 0
+GRID* 356 05.0000000000E+003.5856974341E+00+
+* 3.7873457890E+00 0
+GRID* 357 05.0000000000E+003.0261964422E+01+
+* 5.5797650755E+01 0
+GRID* 358 05.0000000000E+003.4382581526E+01+
+* 5.5796679046E+01 0
+GRID* 359 05.0000000000E+003.9718266944E+00+
+* 1.1028509044E+01 0
+GRID* 360 05.0000000000E+009.3785909726E+00+
+* 9.8439289701E+00 0
+GRID* 361 05.0000000000E+003.9729721919E+00+
+* 1.4572141030E+01 0
+GRID* 362 05.0000000000E+006.9403566321E+00+
+* 1.4094366971E+01 0
+GRID* 363 05.0000000000E+003.8606774847E+01+
+* 5.5841348853E+01 0
+GRID* 364 05.0000000000E+003.8738418782E+01+
+* 5.0814657207E+01 0
+GRID* 365 05.0000000000E+002.6202817093E+01+
+* 5.5900823207E+01 0
+GRID* 366 05.0000000000E+005.9388987152E+01+
+* 4.6035527194E+01 0
+GRID* 367 05.0000000000E+006.3465847580E+01+
+* 4.6031004930E+01 0
+GRID* 368 05.0000000000E+005.5299836544E+01+
+* 4.6039327646E+01 0
+GRID* 369 05.0000000000E+006.7515155196E+01+
+* 4.6026813591E+01 0
+GRID* 370 05.0000000000E+005.1178299379E+01+
+* 5.0996950800E+01 0
+GRID* 371 05.0000000000E+005.1149247397E+01+
+* 5.5937427010E+01 0
+GRID* 372 05.0000000000E+005.5268525166E+01+
+* 5.5942238655E+01 0
+GRID* 373 05.0000000000E+005.5283704844E+01+
+* 5.1000047157E+01 0
+GRID* 374 05.0000000000E+005.1211159137E+01+
+* 4.6041648472E+01 0
+GRID* 375 05.0000000000E+006.3457453995E+01+
+* 5.5940119114E+01 0
+GRID* 376 05.0000000000E+006.7510910272E+01+
+* 5.5937721338E+01 0
+GRID* 377 05.0000000000E+005.9373068539E+01+
+* 5.5942041900E+01 0
+GRID* 378 05.0000000000E+005.9380922316E+01+
+* 5.0997772071E+01 0
+GRID* 379 05.0000000000E+006.3461605101E+01+
+* 5.0993509944E+01 0
+GRID* 380 05.0000000000E+006.7513020038E+01+
+* 5.0989247899E+01 0
+GRID* 381 05.0000000000E+004.7134721663E+01+
+* 4.6037028277E+01 0
+GRID* 382 05.0000000000E+004.3097398515E+01+
+* 4.6000476265E+01 0
+GRID* 383 05.0000000000E+003.9065052534E+01+
+* 4.5841801191E+01 0
+GRID* 384 05.0000000000E+002.7445107621E+01+
+* 3.9446867269E+01 0
+GRID* 385 05.0000000000E+002.5130714920E+01+
+* 3.5814269748E+01 0
+GRID* 386 05.0000000000E+003.0056053841E+01+
+* 4.4465002628E+01 0
+GRID* 387 05.0000000000E+003.4795410630E+01+
+* 4.5350228025E+01 0
+GRID* 388 05.0000000000E+003.4425667459E+01+
+* 5.0638496245E+01 0
+GRID* 389 05.0000000000E+003.0203964212E+01+
+* 5.0566731657E+01 0
+GRID* 390 05.0000000000E+004.8011681810E+00+
+* 2.9262710076E+01 0
+GRID* 391 05.0000000000E+004.5944280103E+00+
+* 2.5533990260E+01 0
+GRID* 392 05.0000000000E+009.2507564628E+00+
+* 2.4835127113E+01 0
+GRID* 393 05.0000000000E+009.8438230211E+00+
+* 2.8549804947E+01 0
+GRID* 394 05.0000000000E+004.5854585543E+00+
+* 3.3052142140E+01 0
+GRID* 395 05.0000000000E+001.0408679431E+01+
+* 3.2314499236E+01 0
+GRID* 396 05.0000000000E+008.5491415315E+00+
+* 2.1211000581E+01 0
+GRID* 397 05.0000000000E+004.3067665318E+00+
+* 2.1899179201E+01 0
+GRID* 398 05.0000000000E+002.6046185981E+01+
+* 5.0799615324E+01 0
+GRID* 399 05.0000000000E+004.0281999484E+00+
+* 1.8212520922E+01 0
+GRID* 400 05.0000000000E+007.7954339287E+00+
+* 1.7612910514E+01 0
+GRID* 401 05.0000000000E+001.1479883680E+01+
+* 1.6586408787E+01 0
+GRID* 402 05.0000000000E+001.0381261056E+01+
+* 1.3190266404E+01 0
+GRID* 403 05.0000000000E+001.2697865566E+01+
+* 2.0062992563E+01 0
+GRID* 404 05.0000000000E+001.3958899880E+01+
+* 2.3572551647E+01 0
+GRID* 405 05.0000000000E+001.6317193205E+01+
+* 1.8592045733E+01 0
+GRID* 406 05.0000000000E+001.4672741051E+01+
+* 1.5287659399E+01 0
+GRID* 407 05.0000000000E+001.1080322048E+01+
+* 3.6048228510E+01 0
+GRID* 408 05.0000000000E+007.3856214512E+00+
+* 3.6567036284E+01 0
+GRID* 409 05.0000000000E+002.3276326697E+01+
+* 3.2305584427E+01 0
+GRID* 410 05.0000000000E+009.1932167638E+01+
+* 4.6015597608E+01 0
+GRID* 411 05.0000000000E+009.5968301975E+01+
+* 4.6014949588E+01 0
+GRID* 412 05.0000000000E+002.1414903547E+01+
+* 2.8827775205E+01 0
+GRID* 413 05.0000000000E+008.3793931702E+01+
+* 4.6017694379E+01 0
+GRID* 414 05.0000000000E+008.7869906920E+01+
+* 4.6016526963E+01 0
+GRID* 415 05.0000000000E+007.1573007002E+01+
+* 5.5936838117E+01 0
+GRID* 416 05.0000000000E+007.1574107304E+01+
+* 5.0987860683E+01 0
+GRID* 417 05.0000000000E+001.7987682711E+01+
+* 2.1934803882E+01 0
+GRID* 418 05.0000000000E+007.5636775211E+01+
+* 5.5934563485E+01 0
+GRID* 419 05.0000000000E+007.5637325362E+01+
+* 5.0984525461E+01 0
+GRID* 420 05.0000000000E+008.3793800311E+01+
+* 5.0980098064E+01 0
+GRID* 421 05.0000000000E+008.7869829166E+01+
+* 5.0978720162E+01 0
+GRID* 422 05.0000000000E+007.9714392142E+01+
+* 5.0982001912E+01 0
+GRID* 423 05.0000000000E+008.3793684812E+01+
+* 5.5931617703E+01 0
+GRID* 424 05.0000000000E+007.9714098187E+01+
+* 5.5932837157E+01 0
+GRID* 425 05.0000000000E+008.7869735646E+01+
+* 5.5930992392E+01 0
+GRID* 426 05.0000000000E+009.1932183314E+01+
+* 5.0978026380E+01 0
+GRID* 427 05.0000000000E+001.9680506791E+01+
+* 2.5364068443E+01 0
+GRID* 428 05.0000000000E+001.5486344273E+01+
+* 2.7041424366E+01 0
+GRID* 429 05.0000000000E+001.7194800368E+01+
+* 3.0539074755E+01 0
+GRID* 430 05.0000000000E+001.3751252504E+01+
+* 3.1553005538E+01 0
+GRID* 431 05.0000000000E+009.5968347869E+01+
+* 5.0977912665E+01 0
+GRID* 432 05.0000000000E+001.8603824805E+01+
+* 3.4176488932E+01 0
+GRID* 433 05.0000000000E+009.1932200421E+01+
+* 5.5930923222E+01 0
+GRID* 434 05.0000000000E+009.5968551480E+01+
+* 5.5931293994E+01 0
+GRID* 435 05.0000000000E+004.4201902522E+00+
+* 5.6687431366E+01 0
+GRID* 436 05.0000000000E+007.9714672836E+01+
+* 4.6019422200E+01 0
+GRID* 437 05.0000000000E+007.5637880937E+01+
+* 4.6021767105E+01 0
+GRID* 438 05.0000000000E+007.1575208023E+01+
+* 4.6025326634E+01 0
+GRID* 439 05.0000000000E+008.8357899269E+00+
+* 5.6606501491E+01 0
+GRID* 440 05.0000000000E+004.3780153764E+00+
+* 5.2506440997E+01 0
+GRID* 441 05.0000000000E+004.2970264175E+00+
+* 4.8453256920E+01 0
+GRID* 442 05.0000000000E+008.7261976681E+00+
+* 5.2343566537E+01 0
+GRID* 443 05.0000000000E+008.5311506319E+00+
+* 4.8241092428E+01 0
+GRID* 444 05.0000000000E+001.4792197561E+01+
+* 3.5287825562E+01 0
+GRID* 445 05.0000000000E+002.5539378040E+01+
+* 4.5550076076E+01 0
+GRID* 446 05.0000000000E+002.4240883522E+01+
+* 4.0958697372E+01 0
+GRID* 447 05.0000000000E+001.9750106774E+01+
+* 3.8006793537E+01 0
+GRID* 448 05.0000000000E+002.0639219604E+01+
+* 4.2145053216E+01 0
+GRID* 449 05.0000000000E+001.5686001412E+01+
+* 3.9101186611E+01 0
+GRID* 450 05.0000000000E+001.6470874003E+01+
+* 4.3102751904E+01 0
+GRID* 451 05.0000000000E+002.1377789165E+01+
+* 4.6561255261E+01 0
+GRID* 452 05.0000000000E+001.7068287963E+01+
+* 4.7295958071E+01 0
+GRID* 453 05.0000000000E+001.2310657286E+01+
+* 4.3795009194E+01 0
+GRID* 454 05.0000000000E+001.1754792922E+01+
+* 3.9859669505E+01 0
+GRID* 455 05.0000000000E+004.1909059657E+00+
+* 4.4483620900E+01 0
+GRID* 456 05.0000000000E+008.2502863932E+00+
+* 4.4242189988E+01 0
+GRID* 457 05.0000000000E+007.8817557025E+00+
+* 4.0348414643E+01 0
+GRID* 458 05.0000000000E+004.1125026121E+00+
+* 4.0620056315E+01 0
+GRID* 459 05.0000000000E+004.0579676845E+00+
+* 3.6836474685E+01 0
+GRID* 460 05.0000000000E+001.2770196775E+01+
+* 4.7829692929E+01 0
+GRID* 461 05.0000000000E+001.7469402323E+01+
+* 5.1689557076E+01 0
+GRID* 462 05.0000000000E+001.3083320656E+01+
+* 5.2070353747E+01 0
+GRID* 463 05.0000000000E+008.3025201051E+00+
+* 6.6601957597E+00 0
+GRID* 464 05.0000000000E+001.3256491402E+01+
+* 5.6471585662E+01 0
+GRID* 465 05.0000000000E+001.7672719846E+01+
+* 5.6289606483E+01 0
+GRID* 466 05.0000000000E+002.1815804283E+01+
+* 5.1233531505E+01 0
+GRID* 467 05.0000000000E+002.2011741917E+01+
+* 5.6083219559E+01 0
+GRID* 468 05.1550769091E-033.8367229898E+00+
+* 7.4285256472E+00 0
+GRID* 469 05.2625962377E-038.3017535866E+00+
+* 6.6582454350E+00 0
+GRID* 470 05.1107794821E-037.4104022887E+00+
+* 3.3885932763E+00 0
+GRID* 471 05.0520875335E-033.5858216484E+00+
+* 3.7861739595E+00 0
+GRID* 472 05.6047753274E-032.6202790686E+01+
+* 5.5900804280E+01 0
+GRID* 473 05.6003981113E-033.0282448972E+01+
+* 5.5810725539E+01 0
+GRID* 474 05.6890935481E-032.6046262869E+01+
+* 5.0799635796E+01 0
+GRID* 475 05.6733094931E-033.0203979353E+01+
+* 5.0566750852E+01 0
+GRID* 476 05.5937223911E-033.4382564122E+01+
+* 5.5796651690E+01 0
+GRID* 477 05.6678742945E-033.4425678129E+01+
+* 5.0638459889E+01 0
+GRID* 478 05.5945754826E-033.8606812756E+01+
+* 5.5841323434E+01 0
+GRID* 479 05.6713574409E-033.8738472308E+01+
+* 5.0814618064E+01 0
+GRID* 480 05.6960151374E-034.7064579842E+01+
+* 5.0980092069E+01 0
+GRID* 481 05.6860090077E-034.2935754355E+01+
+* 5.0930214716E+01 0
+GRID* 482 05.6017950952E-034.7008975175E+01+
+* 5.5922390102E+01 0
+GRID* 483 05.6002267480E-034.2835020121E+01+
+* 5.5890940605E+01 0
+GRID* 484 05.2504033625E-033.9720045527E+00+
+* 1.1028654001E+01 0
+GRID* 485 05.3485088825E-033.9729750507E+00+
+* 1.4571956492E+01 0
+GRID* 486 05.3212621093E-039.3775208876E+00+
+* 9.8436756492E+00 0
+GRID* 487 05.3981260240E-036.9403065053E+00+
+* 1.4094205952E+01 0
+GRID* 488 05.7547070861E-035.9388978332E+01+
+* 4.6035567370E+01 0
+GRID* 489 05.7512537420E-036.3465841025E+01+
+* 4.6031064716E+01 0
+GRID* 490 05.7486721158E-035.5299829646E+01+
+* 4.6039406207E+01 0
+GRID* 491 05.7523042738E-036.7515164495E+01+
+* 4.6026917246E+01 0
+GRID* 492 05.6059040904E-035.1149259789E+01+
+* 5.5937407522E+01 0
+GRID* 493 05.7097689092E-035.1178301451E+01+
+* 5.0996935362E+01 0
+GRID* 494 05.6095586002E-035.5268546833E+01+
+* 5.5942233174E+01 0
+GRID* 495 05.7143957198E-035.5283698826E+01+
+* 5.1000035187E+01 0
+GRID* 496 05.7358161390E-035.1211147664E+01+
+* 4.6041751889E+01 0
+GRID* 497 05.7136022329E-036.7513025642E+01+
+* 5.0989224409E+01 0
+GRID* 498 05.7156250656E-036.3461599380E+01+
+* 5.0993496010E+01 0
+GRID* 499 05.6048908114E-036.3457451612E+01+
+* 5.5940110533E+01 0
+GRID* 500 05.6086272776E-036.7525611267E+01+
+* 5.5939623802E+01 0
+GRID* 501 05.6106091321E-035.9373059749E+01+
+* 5.5942052572E+01 0
+GRID* 502 05.7184041321E-035.9380913406E+01+
+* 5.0997757984E+01 0
+GRID* 503 05.4620967090E-034.5943829768E+00+
+* 2.5534042829E+01 0
+GRID* 504 05.5667475641E-039.2507494301E+00+
+* 2.4835116849E+01 0
+GRID* 505 05.6060307503E-039.8438320518E+00+
+* 2.8549810851E+01 0
+GRID* 506 05.4992155015E-034.8012002163E+00+
+* 2.9262746962E+01 0
+GRID* 507 05.6825072348E-034.3097345825E+01+
+* 4.6000696626E+01 0
+GRID* 508 05.7115794003E-034.7134705869E+01+
+* 4.6037199584E+01 0
+GRID* 509 05.6627445698E-033.9064875032E+01+
+* 4.5841813532E+01 0
+GRID* 510 05.5936106324E-032.5130822265E+01+
+* 3.5815507623E+01 0
+GRID* 511 05.6385525346E-032.7446073303E+01+
+* 3.9448950868E+01 0
+GRID* 512 05.6876555860E-033.0056020642E+01+
+* 4.4465046060E+01 0
+GRID* 513 05.6495495915E-033.4795717711E+01+
+* 4.5350212645E+01 0
+GRID* 514 05.6277678192E-031.0408682293E+01+
+* 3.2314491252E+01 0
+GRID* 515 05.5026576698E-034.5854339653E+00+
+* 3.3052170033E+01 0
+GRID* 516 05.3826846957E-034.3068103989E+00+
+* 2.1899351933E+01 0
+GRID* 517 05.5249349058E-038.5490462244E+00+
+* 2.1210898541E+01 0
+GRID* 518 05.4125279963E-034.0282607430E+00+
+* 1.8212498382E+01 0
+GRID* 519 05.7408378303E-032.1377781295E+01+
+* 4.6561273483E+01 0
+GRID* 520 05.7255194366E-032.5539463931E+01+
+* 4.5550085616E+01 0
+GRID* 521 05.6986824453E-032.1815765300E+01+
+* 5.1233534334E+01 0
+GRID* 522 05.4652892828E-037.7954555657E+00+
+* 1.7612890251E+01 0
+GRID* 523 05.4329723895E-031.0381167179E+01+
+* 1.3190188504E+01 0
+GRID* 524 05.4877751350E-031.1479518941E+01+
+* 1.6585772328E+01 0
+GRID* 525 05.3839326680E-034.2970341642E+00+
+* 4.8453286958E+01 0
+GRID* 526 05.4978445947E-034.1909062021E+00+
+* 4.4483635303E+01 0
+GRID* 527 05.6774482906E-031.3751273762E+01+
+* 3.1552957723E+01 0
+GRID* 528 05.5721194327E-031.3958770759E+01+
+* 2.3572477975E+01 0
+GRID* 529 05.5380032241E-031.2697763285E+01+
+* 2.0062946938E+01 0
+GRID* 530 05.4934897304E-031.6316793136E+01+
+* 1.8592018612E+01 0
+GRID* 531 05.4656022072E-031.4673107735E+01+
+* 1.5284522406E+01 0
+GRID* 532 05.6769006729E-031.1080298207E+01+
+* 3.6048223923E+01 0
+GRID* 533 05.5730991840E-037.3856433836E+00+
+* 3.6567068647E+01 0
+GRID* 534 05.6366340101E-031.5486243300E+01+
+* 2.7041252242E+01 0
+GRID* 535 05.6121514022E-032.3275959291E+01+
+* 3.2305515523E+01 0
+GRID* 536 05.4453068256E-034.0580437382E+00+
+* 3.6836618568E+01 0
+GRID* 537 05.3816825926E-039.5968102416E+01+
+* 4.6015005139E+01 0
+GRID* 538 05.5084653974E-039.1932197382E+01+
+* 4.6015674022E+01 0
+GRID* 539 05.5960432470E-032.1414864205E+01+
+* 2.8827618653E+01 0
+GRID* 540 05.6861058652E-038.3793950606E+01+
+* 4.6017789212E+01 0
+GRID* 541 05.6139842451E-038.7869880934E+01+
+* 4.6016609875E+01 0
+GRID* 542 05.7083048701E-037.1574091868E+01+
+* 5.0987837342E+01 0
+GRID* 543 05.6048982620E-037.1573005931E+01+
+* 5.5936834602E+01 0
+GRID* 544 05.5945792079E-037.5636784540E+01+
+* 5.5934552221E+01 0
+GRID* 545 05.7001613855E-037.5637340741E+01+
+* 5.0984511574E+01 0
+GRID* 546 05.5797339261E-037.9714086297E+01+
+* 5.5932827442E+01 0
+GRID* 547 05.6873761892E-037.9714380192E+01+
+* 5.0981983451E+01 0
+GRID* 548 05.5485904992E-031.7987787940E+01+
+* 2.1934500434E+01 0
+GRID* 549 05.5411101162E-031.9679883443E+01+
+* 2.5362807864E+01 0
+GRID* 550 05.6508459926E-038.3793818067E+01+
+* 5.0980072168E+01 0
+GRID* 551 05.5818126380E-038.7869801510E+01+
+* 5.0978703527E+01 0
+GRID* 552 05.5443660200E-038.3793699245E+01+
+* 5.5931622325E+01 0
+GRID* 553 05.4789387465E-038.7869714130E+01+
+* 5.5930988638E+01 0
+GRID* 554 05.3527594388E-039.5968141040E+01+
+* 5.0977896651E+01 0
+GRID* 555 05.3887829959E-039.1932231118E+01+
+* 5.5930916964E+01 0
+GRID* 556 05.2853614867E-039.5968340715E+01+
+* 5.5931287377E+01 0
+GRID* 557 05.4791883409E-039.1932210972E+01+
+* 5.0978007173E+01 0
+GRID* 558 05.6860872388E-031.7194844596E+01+
+* 3.0538994173E+01 0
+GRID* 559 05.6899354637E-031.8603767641E+01+
+* 3.4176450188E+01 0
+GRID* 560 05.3585112870E-034.4201743954E+00+
+* 5.6687429695E+01 0
+GRID* 561 05.7232842624E-037.9714659202E+01+
+* 4.6019494501E+01 0
+GRID* 562 05.7379209280E-037.5637893664E+01+
+* 4.6021849063E+01 0
+GRID* 563 05.7458893239E-037.1575189487E+01+
+* 4.6025395361E+01 0
+GRID* 564 05.4649800837E-038.8357642949E+00+
+* 5.6606492905E+01 0
+GRID* 565 05.3361073911E-034.3709826668E+00+
+* 5.2513611985E+01 0
+GRID* 566 05.5569724023E-038.7262190640E+00+
+* 5.2343596337E+01 0
+GRID* 567 05.5204384804E-038.5311558750E+00+
+* 4.8241130335E+01 0
+GRID* 568 05.7195924997E-031.4741868965E+01+
+* 3.5286438267E+01 0
+GRID* 569 05.7086103439E-032.4241030508E+01+
+* 4.0959262785E+01 0
+GRID* 570 05.7153158665E-031.9750082754E+01+
+* 3.8006872128E+01 0
+GRID* 571 05.7509035647E-032.0639190577E+01+
+* 4.2145132940E+01 0
+GRID* 572 05.7377085865E-031.5692107044E+01+
+* 3.9120924975E+01 0
+GRID* 573 05.7505869150E-031.6470870129E+01+
+* 4.3102789637E+01 0
+GRID* 574 05.7305299520E-031.7068269364E+01+
+* 4.7295971957E+01 0
+GRID* 575 05.5695154548E-038.2502649334E+00+
+* 4.4242205274E+01 0
+GRID* 576 05.6951285183E-031.1754797033E+01+
+* 3.9859646525E+01 0
+GRID* 577 05.6887582719E-031.2310636780E+01+
+* 4.3795009431E+01 0
+GRID* 578 05.5850797176E-037.8817822245E+00+
+* 4.0348488342E+01 0
+GRID* 579 05.5169218063E-034.1124886625E+00+
+* 4.0620039742E+01 0
+GRID* 580 05.7049036801E-031.2770199157E+01+
+* 4.7829676953E+01 0
+GRID* 581 05.6951545954E-031.7469400235E+01+
+* 5.1689567385E+01 0
+GRID* 582 05.6487300277E-031.3083334423E+01+
+* 5.2070340870E+01 0
+GRID* 583 05.5414155900E-031.3256467796E+01+
+* 5.6471588282E+01 0
+GRID* 584 05.5843532860E-031.7672695287E+01+
+* 5.6289603798E+01 0
+GRID* 585 05.6011506200E-032.2011729457E+01+
+* 5.6083202450E+01 0
+$*
+$* ELEMENT CARDS
+$*
+$* Mesh Collector: Solid(1)
+$* Mesh: 3d_mesh(1)
+CHEXA 1 1 469 163 165 470 468 162+
++ 164 471
+CHEXA 2 1 166 167 168 169 473 472+
++ 474 475
+CHEXA 3 1 164 471 295 30 165 470+
++ 296 31
+CHEXA 4 1 170 477 479 172 171 476+
++ 478 173
+CHEXA 5 1 166 169 170 171 473 475+
++ 477 476
+CHEXA 6 1 350 470 297 109 107 296+
++ 87 10
+CHEXA 7 1 174 175 176 177 481 480+
++ 482 483
+CHEXA 8 1 178 484 485 179 15 280+
++ 281 16
+CHEXA 9 1 483 177 174 481 478 173+
++ 172 479
+CHEXA 10 1 351 483 482 353 352 481+
++ 480 354
+CHEXA 11 1 45 310 311 46 166 473+
++ 472 167
+CHEXA 12 1 92 355 356 93 282 468+
++ 471 283
+CHEXA 13 1 171 166 473 476 47 45+
++ 310 312
+CHEXA 14 1 473 357 358 476 310 122+
++ 123 312
+CHEXA 15 1 360 359 361 362 486 484+
++ 485 487
+CHEXA 16 1 478 363 364 479 483 351+
++ 352 481
+CHEXA 17 1 472 311 124 365 473 310+
++ 122 357
+CHEXA 18 1 478 363 351 483 313 125+
++ 126 314
+CHEXA 19 1 482 483 314 315 176 177+
++ 48 49
+CHEXA 20 1 366 488 489 367 145 333+
++ 334 146
+CHEXA 21 1 490 368 147 335 488 366+
++ 145 333
+CHEXA 22 1 333 68 180 488 334 69+
++ 181 489
+CHEXA 23 1 336 148 369 491 334 146+
++ 367 489
+CHEXA 24 1 371 492 494 372 370 493+
++ 495 373
+CHEXA 25 1 374 496 490 368 149 337+
++ 335 147
+CHEXA 26 1 493 370 374 496 495 373+
++ 368 490
+CHEXA 27 1 492 482 480 493 371 353+
++ 354 370
+CHEXA 28 1 71 335 490 183 70 337+
++ 496 182
+CHEXA 29 1 490 183 180 488 335 71+
++ 68 333
+CHEXA 30 1 491 185 181 489 497 184+
++ 186 498
+CHEXA 31 1 314 313 50 48 483 478+
++ 173 177
+CHEXA 32 1 375 127 128 376 499 316+
++ 317 500
+CHEXA 33 1 123 125 363 358 312 313+
++ 478 476
+CHEXA 34 1 187 188 51 52 499 500+
++ 317 316
+CHEXA 35 1 499 187 189 501 316 52+
++ 53 318
+CHEXA 36 1 499 316 318 501 375 127+
++ 129 377
+CHEXA 37 1 377 501 499 375 378 502+
++ 498 379
+CHEXA 38 1 190 191 192 193 503 504+
++ 505 506
+CHEXA 39 1 499 187 188 500 498 186+
++ 184 497
+CHEXA 40 1 367 369 491 489 379 380+
++ 497 498
+CHEXA 41 1 489 367 379 498 488 366+
++ 378 502
+CHEXA 42 1 186 498 499 187 194 502+
++ 501 189
+CHEXA 43 1 502 378 377 501 495 373+
++ 372 494
+CHEXA 44 1 319 318 129 130 494 501+
++ 377 372
+CHEXA 45 1 315 132 353 482 320 131+
++ 371 492
+CHEXA 46 1 131 320 319 130 371 492+
++ 494 372
+CHEXA 47 1 195 493 496 182 196 495+
++ 490 183
+CHEXA 48 1 492 493 480 482 197 195+
++ 175 176
+CHEXA 49 1 198 494 495 196 189 501+
++ 502 194
+CHEXA 50 1 197 54 320 492 198 55+
++ 319 494
+CHEXA 51 1 199 200 175 174 507 508+
++ 480 481
+CHEXA 52 1 374 149 150 381 496 337+
++ 338 508
+CHEXA 53 1 509 383 12 89 507 382+
++ 151 339
+CHEXA 54 1 507 382 151 339 508 381+
++ 150 338
+CHEXA 55 1 197 492 493 195 198 494+
++ 495 196
+CHEXA 56 1 509 507 481 479 383 382+
++ 352 364
+CHEXA 57 1 338 337 70 72 508 496+
++ 182 200
+CHEXA 58 1 180 488 502 194 183 490+
++ 495 196
+CHEXA 59 1 370 354 480 493 374 381+
++ 508 496
+CHEXA 60 1 189 501 318 53 198 494+
++ 319 55
+CHEXA 61 1 384 511 88 11 385 510+
++ 298 110
+CHEXA 62 1 49 315 482 176 54 320+
++ 492 197
+CHEXA 63 1 201 512 513 202 169 475+
++ 477 170
+CHEXA 64 1 339 507 508 338 73 199+
++ 200 72
+CHEXA 65 1 481 352 382 507 480 354+
++ 381 508
+CHEXA 66 1 493 496 508 480 195 182+
++ 200 175
+CHEXA 67 1 481 174 199 507 479 172+
++ 203 509
+CHEXA 68 1 73 199 203 5 339 507+
++ 509 89
+CHEXA 69 1 489 181 180 488 498 186+
++ 194 502
+CHEXA 70 1 497 498 379 380 500 499+
++ 375 376
+CHEXA 71 1 32 205 510 298 4 204+
++ 511 88
+CHEXA 72 1 509 203 5 89 513 202+
++ 43 308
+CHEXA 73 1 309 512 201 44 88 511+
++ 204 4
+CHEXA 74 1 512 386 384 511 309 120+
++ 11 88
+CHEXA 75 1 509 513 477 479 203 202+
++ 170 172
+CHEXA 76 1 389 386 512 475 388 387+
++ 513 477
+CHEXA 77 1 334 69 181 489 336 74+
++ 185 491
+CHEXA 78 1 509 513 308 89 383 387+
++ 121 12
+CHEXA 79 1 44 43 202 201 309 308+
++ 513 512
+CHEXA 80 1 171 47 312 476 173 50+
++ 313 478
+CHEXA 81 1 126 314 315 132 351 483+
++ 482 353
+CHEXA 82 1 509 479 477 513 383 364+
++ 388 387
+CHEXA 83 1 390 506 505 393 391 503+
++ 504 392
+CHEXA 84 1 390 393 505 506 394 395+
++ 514 515
+CHEXA 85 1 512 386 120 309 513 387+
++ 121 308
+CHEXA 86 1 516 503 391 397 517 504+
++ 392 396
+CHEXA 87 1 95 285 503 391 94 284+
++ 516 397
+CHEXA 88 1 391 95 96 390 503 285+
++ 286 506
+CHEXA 89 1 516 503 190 206 284 285+
++ 17 18
+CHEXA 90 1 389 475 474 398 357 473+
++ 472 365
+CHEXA 91 1 515 287 286 506 394 97+
++ 96 390
+CHEXA 92 1 192 193 506 505 208 207+
++ 515 514
+CHEXA 93 1 193 506 515 207 19 286+
++ 287 20
+CHEXA 94 1 286 19 193 506 285 17+
++ 190 503
+CHEXA 95 1 281 485 179 16 288 518+
++ 209 21
+CHEXA 96 1 206 516 284 18 209 518+
++ 288 21
+CHEXA 97 1 210 520 474 168 211 519+
++ 521 212
+CHEXA 98 1 206 516 518 209 213 517+
++ 522 214
+CHEXA 99 1 487 523 215 217 522 524+
++ 216 214
+CHEXA 100 1 179 485 487 217 209 518+
++ 522 214
+CHEXA 101 1 190 503 516 206 191 504+
++ 517 213
+CHEXA 102 1 516 397 399 518 517 396+
++ 400 522
+CHEXA 103 1 288 284 94 98 518 516+
++ 397 399
+CHEXA 104 1 362 400 522 487 361 399+
++ 518 485
+CHEXA 105 1 288 98 99 281 518 399+
++ 361 485
+CHEXA 106 1 522 400 362 487 524 401+
++ 402 523
+CHEXA 107 1 290 22 219 525 289 23+
++ 218 526
+CPENTA 108 1 192 220 208 505 527 514
+CHEXA 109 1 528 529 403 404 504 517+
++ 396 392
+CHEXA 110 1 401 403 529 524 400 396+
++ 517 522
+CHEXA 111 1 405 530 531 406 403 529+
++ 524 401
+CHEXA 112 1 515 514 532 533 394 395+
++ 407 408
+CPENTA 113 1 111 406 112 299 531 300
+CHEXA 114 1 221 528 534 222 191 504+
++ 505 192
+CHEXA 115 1 385 510 298 110 409 535+
++ 301 113
+CHEXA 116 1 373 495 502 378 368 490+
++ 488 366
+CHEXA 117 1 111 402 401 406 299 523+
++ 524 531
+CHEXA 118 1 536 515 287 291 223 207+
++ 20 24
+CHEXA 119 1 538 410 153 340 537 411+
++ 152 341
+CHEXA 120 1 537 411 152 341 347 159+
++ 13 90
+CHEXA 121 1 75 76 340 341 225 224+
++ 538 537
+CHEXA 122 1 301 113 409 535 302 114+
++ 412 539
+CHEXA 123 1 510 205 32 298 535 226+
++ 33 301
+CHEXA 124 1 90 341 537 347 7 75+
++ 225 82
+CHEXA 125 1 413 540 541 414 154 342+
++ 343 155
+CHEXA 126 1 76 77 343 340 224 227+
++ 541 538
+CHEXA 127 1 535 539 228 226 301 302+
++ 34 33
+CHEXA 128 1 416 542 497 380 415 543+
++ 500 376
+CHEXA 129 1 188 184 229 230 500 497+
++ 542 543
+CHEXA 130 1 56 321 543 230 57 322+
++ 544 231
+CHEXA 131 1 545 544 231 232 542 543+
++ 230 229
+CHEXA 132 1 546 233 231 544 323 58+
++ 57 322
+CHEXA 133 1 547 546 233 234 545 544+
++ 231 232
+CHEXA 134 1 115 116 303 304 405 417+
++ 548 530
+CHEXA 135 1 418 544 543 415 133 322+
++ 321 134
+CHEXA 136 1 406 405 115 112 531 530+
++ 304 300
+CHEXA 137 1 35 36 235 236 303 305+
++ 549 548
+CHEXA 138 1 545 419 418 544 542 416+
++ 415 543
+CHEXA 139 1 128 317 321 134 376 500+
++ 543 415
+CHEXA 140 1 500 543 230 188 317 321+
++ 56 51
+CHEXA 141 1 550 540 541 551 420 413+
++ 414 421
+CHEXA 142 1 550 420 423 552 547 422+
++ 424 546
+CHEXA 143 1 421 551 550 420 425 553+
++ 552 423
+CHEXA 144 1 227 541 551 238 237 540+
++ 550 239
+CHEXA 145 1 550 551 238 239 552 553+
++ 240 241
+CHEXA 146 1 546 552 241 233 323 324+
++ 59 58
+CHEXA 147 1 324 323 546 552 136 135+
++ 424 423
+CHEXA 148 1 137 325 553 425 136 324+
++ 552 423
+CHEXA 149 1 554 242 83 348 537 225+
++ 82 347
+CHEXA 150 1 327 555 244 60 326 556+
++ 243 61
+CHEXA 151 1 227 224 245 238 541 538+
++ 557 551
+CHEXA 152 1 557 555 244 245 551 553+
++ 240 238
+CHEXA 153 1 421 414 541 551 426 410+
++ 538 557
+CHEXA 154 1 557 245 242 554 538 224+
++ 225 537
+CHEXA 155 1 192 222 246 220 505 534+
++ 558 527
+CHEXA 156 1 549 534 222 235 548 528+
++ 221 236
+CHEXA 157 1 427 549 305 117 417 548+
++ 303 116
+CHEXA 158 1 505 534 558 527 393 428+
++ 429 430
+CHEXA 159 1 554 242 243 556 348 83+
++ 84 349
+CPENTA 160 1 505 527 514 393 430 395
+CHEXA 161 1 537 411 431 554 538 410+
++ 426 557
+CHEXA 162 1 34 302 539 228 36 305+
++ 549 235
+CHEXA 163 1 549 235 222 534 539 228+
++ 246 558
+CHEXA 164 1 228 226 247 246 539 535+
++ 559 558
+CHEXA 165 1 244 555 557 245 243 556+
++ 554 242
+CHEXA 166 1 429 428 534 558 412 427+
++ 549 539
+CHEXA 167 1 412 409 535 539 429 432+
++ 559 558
+CHEXA 168 1 327 555 433 138 325 553+
++ 425 137
+CHEXA 169 1 425 553 555 433 421 551+
++ 557 426
+CHEXA 170 1 434 556 554 431 433 555+
++ 557 426
+CHEXA 171 1 114 302 305 117 412 539+
++ 549 427
+CHEXA 172 1 160 434 139 14 349 556+
++ 326 91
+CHEXA 173 1 528 221 236 548 529 248+
++ 249 530
+CHEXA 174 1 560 435 140 328 292 100+
++ 9 86
+CHEXA 175 1 556 243 61 326 349 84+
++ 6 91
+CHEXA 176 1 554 431 161 348 556 434+
++ 160 349
+CHEXA 177 1 392 393 428 404 504 505+
++ 534 528
+CHEXA 178 1 427 549 548 417 428 534+
++ 528 404
+CHEXA 179 1 405 403 404 417 530 529+
++ 528 548
+CHEXA 180 1 154 342 344 156 413 540+
++ 561 436
+CHEXA 181 1 437 562 561 436 157 345+
++ 344 156
+CHEXA 182 1 547 234 239 550 561 250+
++ 237 540
+CHEXA 183 1 545 232 234 547 562 251+
++ 250 561
+CHEXA 184 1 345 344 78 79 562 561+
++ 250 251
+CHEXA 185 1 229 542 563 252 232 545+
++ 562 251
+CHEXA 186 1 185 252 229 184 491 563+
++ 542 497
+CHEXA 187 1 416 542 545 419 438 563+
++ 562 437
+CHEXA 188 1 563 438 158 346 562 437+
++ 157 345
+CHEXA 189 1 563 252 251 562 346 80+
++ 79 345
+CHEXA 190 1 80 74 336 346 252 185+
++ 491 563
+CHEXA 191 1 497 380 369 491 542 416+
++ 438 563
+CHEXA 192 1 369 491 563 438 148 336+
++ 346 158
+CHEXA 193 1 439 564 560 435 141 329+
++ 328 140
+CHEXA 194 1 25 293 290 22 253 565+
++ 525 219
+CHEXA 195 1 565 253 255 566 525 219+
++ 254 567
+CHEXA 196 1 440 565 525 441 101 293+
++ 290 102
+CHEXA 197 1 566 442 439 564 565 440+
++ 435 560
+CHEXA 198 1 440 565 566 442 441 525+
++ 567 443
+CHEXA 199 1 256 255 566 564 257 253+
++ 565 560
+CHEXA 200 1 256 564 329 62 257 560+
++ 328 63
+CHEXA 201 1 26 1 86 292 257 63+
++ 328 560
+CHEXA 202 1 293 25 26 292 565 253+
++ 257 560
+CHEXA 203 1 560 435 100 292 565 440+
++ 101 293
+CHEXA 204 1 432 559 558 429 444 568+
++ 527 430
+CHEXA 205 1 512 386 445 520 511 384+
++ 446 569
+CHEXA 206 1 511 204 258 569 512 201+
++ 210 520
+CHEXA 207 1 385 510 535 409 447 570+
++ 559 432
+CPENTA 208 1 385 446 384 510 569 511
+CHEXA 209 1 385 447 448 446 510 570+
++ 571 569
+CHEXA 210 1 510 205 226 535 570 259+
++ 247 559
+CHEXA 211 1 247 559 568 260 246 558+
++ 527 220
+CPENTA 212 1 205 204 258 510 511 569
+CHEXA 213 1 259 205 258 261 570 510+
++ 569 571
+CHEXA 214 1 444 449 447 432 568 572+
++ 570 559
+CHEXA 215 1 259 570 572 262 247 559+
++ 568 260
+CHEXA 216 1 572 262 263 573 570 259+
++ 261 571
+CHEXA 217 1 571 573 572 570 448 450+
++ 449 447
+CHEXA 218 1 451 519 520 445 448 571+
++ 569 446
+CHEXA 219 1 519 211 210 520 571 261+
++ 258 569
+CHEXA 220 1 263 261 211 264 573 571+
++ 519 574
+CHEXA 221 1 448 571 573 450 451 519+
++ 574 452
+CHEXA 222 1 265 254 219 218 575 567+
++ 525 526
+CHEXA 223 1 576 577 453 454 572 573+
++ 450 449
+CHEXA 224 1 455 456 457 458 526 575+
++ 578 579
+CHEXA 225 1 575 456 457 578 577 453+
++ 454 576
+CHEXA 226 1 525 441 455 526 567 443+
++ 456 575
+CHEXA 227 1 290 102 103 289 525 441+
++ 455 526
+CHEXA 228 1 526 455 103 289 579 458+
++ 104 294
+CHEXA 229 1 104 458 459 105 294 579+
++ 536 291
+CHEXA 230 1 459 408 533 536 458 457+
++ 578 579
+CHEXA 231 1 514 515 533 532 208 207+
++ 266 267
+CPENTA 232 1 515 536 533 207 223 266
+CHEXA 233 1 536 515 394 459 291 287+
++ 97 105
+CHEXA 234 1 531 530 249 268 524 529+
++ 248 216
+CHEXA 235 1 216 248 213 214 524 529+
++ 517 522
+CHEXA 236 1 191 504 517 213 221 528+
++ 529 248
+CPENTA 237 1 515 533 536 394 408 459
+CHEXA 238 1 527 514 532 568 220 208+
++ 267 260
+CHEXA 239 1 407 408 457 454 532 533+
++ 578 576
+CHEXA 240 1 530 304 37 249 548 303+
++ 35 236
+CHEXA 241 1 444 568 527 430 407 532+
++ 514 395
+CHEXA 242 1 444 449 572 568 407 454+
++ 576 532
+CHEXA 243 1 267 266 533 532 270 269+
++ 578 576
+CHEXA 244 1 291 294 579 536 24 27+
++ 271 223
+CHEXA 245 1 578 269 266 533 579 271+
++ 223 536
+CHEXA 246 1 572 568 532 576 262 260+
++ 267 270
+CHEXA 247 1 576 577 573 572 270 272+
++ 263 262
+CHEXA 248 1 215 38 268 216 523 299+
++ 531 524
+CHEXA 249 1 304 37 39 300 530 249+
++ 268 531
+CHEXA 250 1 272 577 575 265 270 576+
++ 578 269
+CHEXA 251 1 27 23 289 294 271 218+
++ 526 579
+CHEXA 252 1 218 526 579 271 265 575+
++ 578 269
+CHEXA 253 1 460 580 574 452 453 577+
++ 573 450
+CPENTA 254 1 38 39 268 299 300 531
+CHEXA 255 1 283 471 356 93 85 295+
++ 108 8
+CPENTA 256 1 360 362 402 486 487 523
+CHEXA 257 1 360 486 523 402 118 306+
++ 299 111
+CHEXA 258 1 422 547 550 420 436 561+
++ 540 413
+CHEXA 259 1 453 456 443 460 577 575+
++ 567 580
+CHEXA 260 1 344 561 540 342 78 250+
++ 237 81
+CHEXA 261 1 545 419 437 562 547 422+
++ 436 561
+CHEXA 262 1 581 461 462 582 574 452+
++ 460 580
+CHEXA 263 1 118 306 307 119 360 486+
++ 469 463
+CHEXA 264 1 280 484 359 106 282 468+
++ 355 92
+CHEXA 265 1 566 442 443 567 582 462+
++ 460 580
+CHEXA 266 1 564 439 442 566 583 464+
++ 462 582
+CHEXA 267 1 359 484 486 360 355 468+
++ 469 463
+CHEXA 268 1 433 555 327 138 434 556+
++ 326 139
+CHEXA 269 1 523 215 38 299 486 273+
++ 40 306
+CHEXA 270 1 461 581 582 462 465 584+
++ 583 464
+CHEXA 271 1 554 537 347 348 431 411+
++ 159 161
+CHEXA 272 1 324 59 64 325 552 241+
++ 240 553
+CHEXA 273 1 64 325 553 240 60 327+
++ 555 244
+CHEXA 274 1 178 484 468 162 273 486+
++ 469 163
+CHEXA 275 1 359 106 99 361 484 280+
++ 281 485
+CHEXA 276 1 28 15 280 282 162 178+
++ 484 468
+CHEXA 277 1 201 512 475 169 210 520+
++ 474 168
+CHEXA 278 1 389 386 445 398 475 512+
++ 520 474
+CPENTA 279 1 486 487 523 273 217 215
+CHEXA 280 1 461 452 574 581 466 451+
++ 519 521
+CHEXA 281 1 445 520 519 451 398 474+
++ 521 466
+CHEXA 282 1 398 474 521 466 365 472+
++ 585 467
+CHEXA 283 1 486 484 485 487 273 178+
++ 179 217
+CHEXA 284 1 233 241 552 546 234 239+
++ 550 547
+CHEXA 285 1 474 168 212 521 472 167+
++ 274 585
+CHEXA 286 1 585 467 143 330 584 465+
++ 142 331
+CHEXA 287 1 521 466 467 585 581 461+
++ 465 584
+CHEXA 288 1 365 472 585 467 124 311+
++ 330 143
+CHEXA 289 1 567 254 275 580 575 265+
++ 272 577
+CHEXA 290 1 573 263 272 577 574 264+
++ 275 580
+CHEXA 291 1 307 306 40 41 469 486+
++ 273 163
+CHEXA 292 1 471 356 108 295 470 350+
++ 107 296
+CHEXA 293 1 470 471 356 350 469 468+
++ 355 463
+CHEXA 294 1 163 469 470 165 41 307+
++ 297 42
+CHEXA 295 1 133 322 323 135 418 544+
++ 546 424
+CHEXA 296 1 422 547 545 419 424 546+
++ 544 418
+CHEXA 297 1 350 463 119 109 470 469+
++ 307 297
+CHEXA 298 1 275 580 582 276 254 567+
++ 566 255
+CHEXA 299 1 519 574 581 521 211 264+
++ 277 212
+CHEXA 300 1 42 165 31 3 297 470+
++ 296 87
+CHEXA 301 1 277 581 521 212 278 584+
++ 585 274
+CHEXA 302 1 581 277 264 574 582 276+
++ 275 580
+CHEXA 303 1 279 65 332 583 278 66+
++ 331 584
+CHEXA 304 1 282 28 162 468 283 29+
++ 164 471
+CHEXA 305 1 540 237 227 541 342 81+
++ 77 343
+CHEXA 306 1 276 279 256 255 582 583+
++ 564 566
+CHEXA 307 1 541 414 155 343 538 410+
++ 153 340
+CHEXA 308 1 277 581 584 278 276 582+
++ 583 279
+CHEXA 309 1 564 329 332 583 256 62+
++ 65 279
+CHEXA 310 1 332 144 141 329 583 464+
++ 439 564
+CHEXA 311 1 583 464 465 584 332 144+
++ 142 331
+CHEXA 312 1 46 167 472 311 67 274+
++ 585 330
+CHEXA 313 1 585 274 278 584 330 67+
++ 66 331
+CHEXA 314 1 471 164 30 295 283 29+
++ 2 85
+CHEXA 315 1 477 388 364 479 476 358+
++ 363 478
+CHEXA 316 1 477 476 473 475 388 358+
++ 357 389
+$*
+$* PROPERTY CARDS
+$*
+$* Property: PSOLID1::Bracket_fem1::[1]
+PSOLID 1 1 0 SMECH
+$*
+$* MATERIAL CARDS
+$*
+$* Material: Aluminum_6061::Bracket_fem1::[1]
+MAT1 16.8980+7 0.3300002.7110-62.2380-5
+MATT1 1 1 2 3
+TABLEM1 1 1 +
++ 20.00006.8980+7 21.11006.8980+7 23.89006.8980+7 37.78006.8290+7+
++ 51.67006.8290+7 65.56006.7600+7 79.44006.6910+7 93.33006.6221+7+
++ 107.22006.5531+7121.11006.4841+7135.00006.4151+7148.89006.3461+7+
++ 162.78006.2772+7176.67006.1392+7190.56006.0702+7204.44005.9323+7+
++ 218.33005.8633+7232.22005.7253+7246.11005.5874+7260.00005.4494+7+
++ 273.89005.3114+7287.78005.1735+7301.67005.0355+7315.56004.8976+7+
++ 329.44004.7596+7343.33004.6216+7357.22004.4147+7371.11004.2767+7+
++ 385.00004.1388+7398.89003.9318+7412.78003.7939+7426.67003.5869+7+
++ ENDT
+TABLEM1 2 1 +
++ 20.00000.330000 21.11000.330000 ENDT
+TABLEM1 3 1 +
++ 20.00002.2380-5 93.33002.3184-5107.22002.3346-5121.11002.3508-5+
++ 135.00002.3670-5148.89002.3832-5162.78002.3994-5176.67002.4138-5+
++ 190.56002.4300-5204.44002.4444-5218.33002.4588-5232.22002.4732-5+
++ 246.11002.4876-5260.00002.5002-5273.89002.5146-5287.78002.5272-5+
++ 301.67002.5398-5315.56002.5524-5 ENDT
+$*
+$* LOAD AND CONSTRAINT CARDS
+$*
+$* Load: Force(1)
+FORCE 1 6 02.5026+5 0.0000 0.0000-1.00000
+FORCE 1 14 02.4974+5 0.0000 0.0000-1.00000
+FORCE 1 91 05.0000+5 0.0000 0.0000-1.00000
+$* Constraint: Fixed(1)
+SPC 2 1 123456 0.0000
+SPC 2 2 123456 0.0000
+SPC 2 8 123456 0.0000
+SPC 2 9 123456 0.0000
+SPC 2 15 123456 0.0000
+SPC 2 16 123456 0.0000
+SPC 2 17 123456 0.0000
+SPC 2 18 123456 0.0000
+SPC 2 19 123456 0.0000
+SPC 2 20 123456 0.0000
+SPC 2 21 123456 0.0000
+SPC 2 22 123456 0.0000
+SPC 2 23 123456 0.0000
+SPC 2 24 123456 0.0000
+SPC 2 25 123456 0.0000
+SPC 2 26 123456 0.0000
+SPC 2 27 123456 0.0000
+SPC 2 28 123456 0.0000
+SPC 2 29 123456 0.0000
+SPC 2 85 123456 0.0000
+SPC 2 86 123456 0.0000
+SPC 2 92 123456 0.0000
+SPC 2 93 123456 0.0000
+SPC 2 94 123456 0.0000
+SPC 2 95 123456 0.0000
+SPC 2 96 123456 0.0000
+SPC 2 97 123456 0.0000
+SPC 2 98 123456 0.0000
+SPC 2 99 123456 0.0000
+SPC 2 100 123456 0.0000
+SPC 2 101 123456 0.0000
+SPC 2 102 123456 0.0000
+SPC 2 103 123456 0.0000
+SPC 2 104 123456 0.0000
+SPC 2 105 123456 0.0000
+SPC 2 106 123456 0.0000
+SPC 2 280 123456 0.0000
+SPC 2 281 123456 0.0000
+SPC 2 282 123456 0.0000
+SPC 2 283 123456 0.0000
+SPC 2 284 123456 0.0000
+SPC 2 285 123456 0.0000
+SPC 2 286 123456 0.0000
+SPC 2 287 123456 0.0000
+SPC 2 288 123456 0.0000
+SPC 2 289 123456 0.0000
+SPC 2 290 123456 0.0000
+SPC 2 291 123456 0.0000
+SPC 2 292 123456 0.0000
+SPC 2 293 123456 0.0000
+SPC 2 294 123456 0.0000
+ENDDATA 4ea11cef
diff --git a/tests/mcp_server/__init__.py b/tests/mcp_server/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/mcp_server/tools/__init__.py b/tests/mcp_server/tools/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/mcp_server/tools/test_model_discovery.py b/tests/mcp_server/tools/test_model_discovery.py
new file mode 100644
index 00000000..8c22cdf6
--- /dev/null
+++ b/tests/mcp_server/tools/test_model_discovery.py
@@ -0,0 +1,211 @@
+"""
+Unit tests for MCP Model Discovery Tool
+
+Tests the .sim file parser and FEA model discovery functionality.
+"""
+
+import pytest
+from pathlib import Path
+import sys
+
+# Add project root to path
+project_root = Path(__file__).parent.parent.parent.parent
+sys.path.insert(0, str(project_root))
+
+from mcp_server.tools.model_discovery import (
+ discover_fea_model,
+ format_discovery_result_for_llm,
+ SimFileParser
+)
+
+
+class TestSimFileParser:
+ """Test the SimFileParser class"""
+
+ @pytest.fixture
+ def example_sim_path(self):
+ """Path to example .sim file"""
+ return project_root / "examples" / "test_bracket.sim"
+
+ def test_parser_initialization(self, example_sim_path):
+ """Test that parser initializes correctly"""
+ parser = SimFileParser(example_sim_path)
+ assert parser.sim_path.exists()
+ assert parser.tree is not None
+ assert parser.root is not None
+
+ def test_parser_file_not_found(self):
+ """Test error handling for missing file"""
+ with pytest.raises(FileNotFoundError):
+ SimFileParser("/nonexistent/path/file.sim")
+
+ def test_parser_invalid_extension(self):
+ """Test error handling for non-.sim file"""
+ with pytest.raises(ValueError):
+ SimFileParser(project_root / "README.md")
+
+ def test_extract_solutions(self, example_sim_path):
+ """Test solution extraction"""
+ parser = SimFileParser(example_sim_path)
+ solutions = parser.extract_solutions()
+
+ assert len(solutions) > 0
+ assert solutions[0]['name'] == 'Structural Analysis 1'
+ assert solutions[0]['type'] == 'Static Structural'
+ assert solutions[0]['solver'] == 'NX Nastran'
+
+ def test_extract_expressions(self, example_sim_path):
+ """Test expression extraction"""
+ parser = SimFileParser(example_sim_path)
+ expressions = parser.extract_expressions()
+
+ assert len(expressions) > 0
+
+ # Check for expected expressions
+ expr_names = [e['name'] for e in expressions]
+ assert 'wall_thickness' in expr_names
+ assert 'hole_diameter' in expr_names
+ assert 'rib_spacing' in expr_names
+
+ # Check expression values
+ wall_thickness = next(e for e in expressions if e['name'] == 'wall_thickness')
+ assert wall_thickness['value'] == '5.0'
+ assert wall_thickness['units'] == 'mm'
+
+ def test_extract_fem_info(self, example_sim_path):
+ """Test FEM information extraction"""
+ parser = SimFileParser(example_sim_path)
+ fem_info = parser.extract_fem_info()
+
+ # Check mesh info
+ assert 'mesh' in fem_info
+ assert fem_info['mesh']['name'] == 'Bracket Mesh'
+ assert fem_info['mesh']['node_count'] == '8234'
+ assert fem_info['mesh']['element_count'] == '4521'
+
+ # Check materials
+ assert len(fem_info['materials']) > 0
+ assert fem_info['materials'][0]['name'] == 'Aluminum 6061-T6'
+
+ # Check loads
+ assert len(fem_info['loads']) > 0
+ assert fem_info['loads'][0]['name'] == 'Applied Force'
+
+ # Check constraints
+ assert len(fem_info['constraints']) > 0
+ assert fem_info['constraints'][0]['name'] == 'Fixed Support'
+
+
+class TestDiscoverFEAModel:
+ """Test the main discover_fea_model function"""
+
+ @pytest.fixture
+ def example_sim_path(self):
+ """Path to example .sim file"""
+ return str(project_root / "examples" / "test_bracket.sim")
+
+ def test_successful_discovery(self, example_sim_path):
+ """Test successful model discovery"""
+ result = discover_fea_model(example_sim_path)
+
+ assert result['status'] == 'success'
+ assert result['file_exists'] is True
+ assert 'solutions' in result
+ assert 'expressions' in result
+ assert 'fem_info' in result
+ assert 'summary' in result
+
+ # Check summary statistics
+ assert result['summary']['solution_count'] >= 1
+ assert result['summary']['expression_count'] >= 3
+
+ def test_file_not_found_error(self):
+ """Test error handling for missing file"""
+ result = discover_fea_model("/nonexistent/file.sim")
+
+ assert result['status'] == 'error'
+ assert result['error_type'] == 'file_not_found'
+ assert 'message' in result
+ assert 'suggestion' in result
+
+ def test_result_structure(self, example_sim_path):
+ """Test that result has expected structure"""
+ result = discover_fea_model(example_sim_path)
+
+ # Check top-level keys
+ expected_keys = ['status', 'sim_file', 'file_exists', 'solutions',
+ 'expressions', 'fem_info', 'linked_files', 'metadata', 'summary']
+
+ for key in expected_keys:
+ assert key in result, f"Missing key: {key}"
+
+ # Check summary keys
+ expected_summary_keys = ['solution_count', 'expression_count',
+ 'material_count', 'load_count', 'constraint_count']
+
+ for key in expected_summary_keys:
+ assert key in result['summary'], f"Missing summary key: {key}"
+
+
+class TestFormatDiscoveryResult:
+ """Test the Markdown formatting function"""
+
+ @pytest.fixture
+ def example_sim_path(self):
+ """Path to example .sim file"""
+ return str(project_root / "examples" / "test_bracket.sim")
+
+ def test_format_success_result(self, example_sim_path):
+ """Test formatting of successful discovery"""
+ result = discover_fea_model(example_sim_path)
+ formatted = format_discovery_result_for_llm(result)
+
+ assert isinstance(formatted, str)
+ assert '# FEA Model Analysis' in formatted
+ assert 'Solutions' in formatted
+ assert 'Expressions' in formatted
+ assert 'wall_thickness' in formatted
+
+ def test_format_error_result(self):
+ """Test formatting of error result"""
+ result = discover_fea_model("/nonexistent/file.sim")
+ formatted = format_discovery_result_for_llm(result)
+
+ assert isinstance(formatted, str)
+ assert '❌' in formatted or 'Error' in formatted
+ assert result['message'] in formatted
+
+
+# Integration test
+def test_end_to_end_workflow():
+ """
+ Test the complete workflow:
+ 1. Discover model
+ 2. Format for LLM
+ 3. Verify output is useful
+ """
+ example_sim = str(project_root / "examples" / "test_bracket.sim")
+
+ # Step 1: Discover
+ result = discover_fea_model(example_sim)
+ assert result['status'] == 'success'
+
+ # Step 2: Format
+ formatted = format_discovery_result_for_llm(result)
+ assert len(formatted) > 100 # Should be substantial output
+
+ # Step 3: Verify key information is present
+ assert 'wall_thickness' in formatted
+ assert 'Aluminum' in formatted
+ assert 'Static Structural' in formatted
+
+ print("\n" + "="*60)
+ print("INTEGRATION TEST OUTPUT:")
+ print("="*60)
+ print(formatted)
+ print("="*60)
+
+
+if __name__ == "__main__":
+ # Run tests with pytest
+ pytest.main([__file__, "-v", "-s"])