BREAKING CHANGE: Module paths have been reorganized for better maintainability. Backwards compatibility aliases with deprecation warnings are provided. New Structure: - core/ - Optimization runners (runner, intelligent_optimizer, etc.) - processors/ - Data processing - surrogates/ - Neural network surrogates - nx/ - NX/Nastran integration (solver, updater, session_manager) - study/ - Study management (creator, wizard, state, reset) - reporting/ - Reports and analysis (visualizer, report_generator) - config/ - Configuration management (manager, builder) - utils/ - Utilities (logger, auto_doc, etc.) - future/ - Research/experimental code Migration: - ~200 import changes across 125 files - All __init__.py files use lazy loading to avoid circular imports - Backwards compatibility layer supports old import paths with warnings - All existing functionality preserved To migrate existing code: OLD: from optimization_engine.nx_solver import NXSolver NEW: from optimization_engine.nx.solver import NXSolver OLD: from optimization_engine.runner import OptimizationRunner NEW: from optimization_engine.core.runner import OptimizationRunner 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
403 lines
13 KiB
Python
403 lines
13 KiB
Python
"""
|
|
pyNastran Research Agent - Phase 3
|
|
|
|
Automated research and code generation for OP2 result extraction using pyNastran.
|
|
|
|
This agent:
|
|
1. Searches pyNastran documentation
|
|
2. Finds relevant APIs for extraction tasks
|
|
3. Generates executable Python code for extractors
|
|
4. Stores patterns in knowledge base
|
|
|
|
Author: Atomizer Development Team
|
|
Version: 0.1.0 (Phase 3)
|
|
Last Updated: 2025-01-16
|
|
"""
|
|
|
|
from typing import Dict, Any, List, Optional
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
import json
|
|
|
|
|
|
@dataclass
|
|
class ExtractionPattern:
|
|
"""Represents a learned pattern for OP2 extraction."""
|
|
name: str
|
|
description: str
|
|
element_type: Optional[str] # e.g., 'CBAR', 'CQUAD4', None for general
|
|
result_type: str # 'force', 'stress', 'displacement', 'strain'
|
|
code_template: str
|
|
api_path: str # e.g., 'model.cbar_force[subcase]'
|
|
data_structure: str # Description of data array structure
|
|
examples: List[str] # Example usage
|
|
|
|
|
|
class PyNastranResearchAgent:
|
|
"""
|
|
Research agent for pyNastran documentation and code generation.
|
|
|
|
Uses a combination of:
|
|
- Pre-learned patterns from documentation
|
|
- WebFetch for dynamic lookup (future)
|
|
- Knowledge base caching
|
|
"""
|
|
|
|
def __init__(self, knowledge_base_path: Optional[Path] = None):
|
|
"""
|
|
Initialize the research agent.
|
|
|
|
Args:
|
|
knowledge_base_path: Path to store learned patterns
|
|
"""
|
|
if knowledge_base_path is None:
|
|
knowledge_base_path = Path(__file__).parent.parent / "knowledge_base" / "pynastran_patterns"
|
|
|
|
self.knowledge_base_path = Path(knowledge_base_path)
|
|
self.knowledge_base_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Initialize with core patterns from documentation research
|
|
self.patterns = self._initialize_core_patterns()
|
|
|
|
def _initialize_core_patterns(self) -> Dict[str, ExtractionPattern]:
|
|
"""Initialize core extraction patterns from pyNastran docs."""
|
|
patterns = {}
|
|
|
|
# Displacement extraction
|
|
patterns['displacement'] = ExtractionPattern(
|
|
name='displacement',
|
|
description='Extract displacement results',
|
|
element_type=None,
|
|
result_type='displacement',
|
|
code_template='''def extract_displacement(op2_file: Path, subcase: int = 1):
|
|
"""Extract displacement results from OP2 file."""
|
|
from pyNastran.op2.op2 import OP2
|
|
import numpy as np
|
|
|
|
model = OP2()
|
|
model.read_op2(str(op2_file))
|
|
|
|
disp = model.displacements[subcase]
|
|
itime = 0 # static case
|
|
|
|
# Extract translation components
|
|
txyz = disp.data[itime, :, :3] # [tx, ty, tz]
|
|
|
|
# Calculate total displacement
|
|
total_disp = np.linalg.norm(txyz, axis=1)
|
|
max_disp = np.max(total_disp)
|
|
|
|
# Get node info
|
|
node_ids = [nid for (nid, grid_type) in disp.node_gridtype]
|
|
max_disp_node = node_ids[np.argmax(total_disp)]
|
|
|
|
return {
|
|
'max_displacement': float(max_disp),
|
|
'max_disp_node': int(max_disp_node),
|
|
'max_disp_x': float(np.max(np.abs(txyz[:, 0]))),
|
|
'max_disp_y': float(np.max(np.abs(txyz[:, 1]))),
|
|
'max_disp_z': float(np.max(np.abs(txyz[:, 2])))
|
|
}''',
|
|
api_path='model.displacements[subcase]',
|
|
data_structure='data[itime, :, :6] where :6=[tx, ty, tz, rx, ry, rz]',
|
|
examples=['max_disp = extract_displacement(Path("results.op2"))']
|
|
)
|
|
|
|
# Stress extraction (solid elements)
|
|
patterns['solid_stress'] = ExtractionPattern(
|
|
name='solid_stress',
|
|
description='Extract stress from solid elements (CTETRA, CHEXA)',
|
|
element_type='CTETRA',
|
|
result_type='stress',
|
|
code_template='''def extract_solid_stress(op2_file: Path, subcase: int = 1, element_type: str = 'ctetra'):
|
|
"""Extract stress from solid elements."""
|
|
from pyNastran.op2.op2 import OP2
|
|
import numpy as np
|
|
|
|
model = OP2()
|
|
model.read_op2(str(op2_file))
|
|
|
|
# Get stress object for element type
|
|
# In pyNastran, stress is stored in model.op2_results.stress
|
|
stress_attr = f"{element_type}_stress"
|
|
|
|
if not hasattr(model, 'op2_results') or not hasattr(model.op2_results, 'stress'):
|
|
raise ValueError(f"No stress results in OP2")
|
|
|
|
stress_obj = model.op2_results.stress
|
|
if not hasattr(stress_obj, stress_attr):
|
|
raise ValueError(f"No {element_type} stress results in OP2")
|
|
|
|
stress = getattr(stress_obj, stress_attr)[subcase]
|
|
itime = 0
|
|
|
|
# Extract von Mises if available
|
|
if stress.is_von_mises: # Property, not method
|
|
von_mises = stress.data[itime, :, 9] # Column 9 is von Mises
|
|
max_stress = float(np.max(von_mises))
|
|
|
|
# Get element info
|
|
element_ids = [eid for (eid, node) in stress.element_node]
|
|
max_stress_elem = element_ids[np.argmax(von_mises)]
|
|
|
|
return {
|
|
'max_von_mises': max_stress,
|
|
'max_stress_element': int(max_stress_elem)
|
|
}
|
|
else:
|
|
raise ValueError("von Mises stress not available")''',
|
|
api_path='model.ctetra_stress[subcase] or model.chexa_stress[subcase]',
|
|
data_structure='data[itime, :, 10] where column 9=von_mises',
|
|
examples=['stress = extract_solid_stress(Path("results.op2"), element_type="ctetra")']
|
|
)
|
|
|
|
# CBAR force extraction
|
|
patterns['cbar_force'] = ExtractionPattern(
|
|
name='cbar_force',
|
|
description='Extract forces from CBAR elements',
|
|
element_type='CBAR',
|
|
result_type='force',
|
|
code_template='''def extract_cbar_force(op2_file: Path, subcase: int = 1, direction: str = 'Z'):
|
|
"""
|
|
Extract forces from CBAR elements.
|
|
|
|
Args:
|
|
op2_file: Path to OP2 file
|
|
subcase: Subcase ID
|
|
direction: Force direction ('X', 'Y', 'Z', 'axial', 'torque')
|
|
|
|
Returns:
|
|
Dict with force statistics
|
|
"""
|
|
from pyNastran.op2.op2 import OP2
|
|
import numpy as np
|
|
|
|
model = OP2()
|
|
model.read_op2(str(op2_file))
|
|
|
|
if not hasattr(model, 'cbar_force'):
|
|
raise ValueError("No CBAR force results in OP2")
|
|
|
|
force = model.cbar_force[subcase]
|
|
itime = 0
|
|
|
|
# CBAR force data structure:
|
|
# [bending_moment_a1, bending_moment_a2,
|
|
# bending_moment_b1, bending_moment_b2,
|
|
# shear1, shear2, axial, torque]
|
|
|
|
direction_map = {
|
|
'shear1': 4,
|
|
'shear2': 5,
|
|
'axial': 6,
|
|
'Z': 6, # Commonly axial is Z direction
|
|
'torque': 7
|
|
}
|
|
|
|
col_idx = direction_map.get(direction, direction_map.get(direction.lower(), 6))
|
|
forces = force.data[itime, :, col_idx]
|
|
|
|
return {
|
|
f'max_{direction}_force': float(np.max(np.abs(forces))),
|
|
f'avg_{direction}_force': float(np.mean(np.abs(forces))),
|
|
f'min_{direction}_force': float(np.min(np.abs(forces))),
|
|
'forces_array': forces.tolist()
|
|
}''',
|
|
api_path='model.cbar_force[subcase]',
|
|
data_structure='data[ntimes, nelements, 8] where 8=[bm_a1, bm_a2, bm_b1, bm_b2, shear1, shear2, axial, torque]',
|
|
examples=['forces = extract_cbar_force(Path("results.op2"), direction="Z")']
|
|
)
|
|
|
|
return patterns
|
|
|
|
def research_extraction(self, request: Dict[str, Any]) -> ExtractionPattern:
|
|
"""
|
|
Research and find/generate extraction pattern for a request.
|
|
|
|
Args:
|
|
request: Dict with:
|
|
- action: e.g., 'extract_1d_element_forces'
|
|
- domain: e.g., 'result_extraction'
|
|
- params: {'element_types': ['CBAR'], 'result_type': 'element_force', 'direction': 'Z'}
|
|
|
|
Returns:
|
|
ExtractionPattern with code template
|
|
"""
|
|
action = request.get('action', '')
|
|
params = request.get('params', {})
|
|
|
|
# Determine result type
|
|
if 'displacement' in action.lower():
|
|
return self.patterns['displacement']
|
|
|
|
elif 'stress' in action.lower():
|
|
element_types = params.get('element_types', [])
|
|
if any(et in ['CTETRA', 'CHEXA', 'CPENTA'] for et in element_types):
|
|
return self.patterns['solid_stress']
|
|
# Could add plate stress pattern here
|
|
return self.patterns['solid_stress'] # Default to solid for now
|
|
|
|
elif 'force' in action.lower() or 'element_force' in params.get('result_type', ''):
|
|
element_types = params.get('element_types', [])
|
|
if 'CBAR' in element_types or '1d' in action.lower():
|
|
return self.patterns['cbar_force']
|
|
|
|
# Fallback: return generic pattern
|
|
return self._generate_generic_pattern(request)
|
|
|
|
def _generate_generic_pattern(self, request: Dict[str, Any]) -> ExtractionPattern:
|
|
"""Generate a generic extraction pattern as fallback."""
|
|
return ExtractionPattern(
|
|
name='generic_extraction',
|
|
description=f"Generic extraction for {request.get('action', 'unknown')}",
|
|
element_type=None,
|
|
result_type='unknown',
|
|
code_template='''def extract_generic(op2_file: Path):
|
|
"""Generic OP2 extraction - needs customization."""
|
|
from pyNastran.op2.op2 import OP2
|
|
|
|
model = OP2()
|
|
model.read_op2(str(op2_file))
|
|
|
|
# TODO: Customize extraction based on requirements
|
|
# Available: model.displacements, model.ctetra_stress, etc.
|
|
# Use model.get_op2_stats() to see available results
|
|
|
|
return {'result': None}''',
|
|
api_path='model.<result_type>[subcase]',
|
|
data_structure='Varies by result type',
|
|
examples=['# Needs customization']
|
|
)
|
|
|
|
def generate_extractor_code(self, request: Dict[str, Any]) -> str:
|
|
"""
|
|
Generate complete extractor code for a request.
|
|
|
|
Args:
|
|
request: Extraction request from Phase 2.7 LLM
|
|
|
|
Returns:
|
|
Complete Python code as string
|
|
"""
|
|
pattern = self.research_extraction(request)
|
|
|
|
# Generate module header
|
|
description = request.get('description', pattern.description)
|
|
|
|
code = f'''"""
|
|
{description}
|
|
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
|
|
|
|
Pattern: {pattern.name}
|
|
Element Type: {pattern.element_type or 'General'}
|
|
Result Type: {pattern.result_type}
|
|
API: {pattern.api_path}
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, Any
|
|
import numpy as np
|
|
from pyNastran.op2.op2 import OP2
|
|
|
|
|
|
{pattern.code_template}
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# Example usage
|
|
import sys
|
|
if len(sys.argv) > 1:
|
|
op2_file = Path(sys.argv[1])
|
|
result = {pattern.code_template.split('(')[0].split()[-1]}(op2_file)
|
|
print(f"Extraction result: {{result}}")
|
|
else:
|
|
print("Usage: python {{sys.argv[0]}} <op2_file>")
|
|
'''
|
|
|
|
return code
|
|
|
|
def save_pattern(self, pattern: ExtractionPattern):
|
|
"""Save a pattern to the knowledge base."""
|
|
pattern_file = self.knowledge_base_path / f"{pattern.name}.json"
|
|
|
|
pattern_dict = {
|
|
'name': pattern.name,
|
|
'description': pattern.description,
|
|
'element_type': pattern.element_type,
|
|
'result_type': pattern.result_type,
|
|
'code_template': pattern.code_template,
|
|
'api_path': pattern.api_path,
|
|
'data_structure': pattern.data_structure,
|
|
'examples': pattern.examples
|
|
}
|
|
|
|
with open(pattern_file, 'w') as f:
|
|
json.dump(pattern_dict, f, indent=2)
|
|
|
|
def load_pattern(self, name: str) -> Optional[ExtractionPattern]:
|
|
"""Load a pattern from the knowledge base."""
|
|
pattern_file = self.knowledge_base_path / f"{name}.json"
|
|
|
|
if not pattern_file.exists():
|
|
return None
|
|
|
|
with open(pattern_file, 'r') as f:
|
|
data = json.load(f)
|
|
|
|
return ExtractionPattern(**data)
|
|
|
|
|
|
def main():
|
|
"""Test the pyNastran research agent."""
|
|
print("=" * 80)
|
|
print("Phase 3: pyNastran Research Agent Test")
|
|
print("=" * 80)
|
|
print()
|
|
|
|
agent = PyNastranResearchAgent()
|
|
|
|
# Test request: CBAR force extraction (from Phase 2.7 example)
|
|
test_request = {
|
|
"action": "extract_1d_element_forces",
|
|
"domain": "result_extraction",
|
|
"description": "Extract element forces from CBAR in Z direction from OP2",
|
|
"params": {
|
|
"element_types": ["CBAR"],
|
|
"result_type": "element_force",
|
|
"direction": "Z"
|
|
}
|
|
}
|
|
|
|
print("Test Request:")
|
|
print(f" Action: {test_request['action']}")
|
|
print(f" Description: {test_request['description']}")
|
|
print()
|
|
|
|
print("1. Researching extraction pattern...")
|
|
pattern = agent.research_extraction(test_request)
|
|
print(f" Found pattern: {pattern.name}")
|
|
print(f" API path: {pattern.api_path}")
|
|
print()
|
|
|
|
print("2. Generating extractor code...")
|
|
code = agent.generate_extractor_code(test_request)
|
|
print()
|
|
|
|
print("=" * 80)
|
|
print("Generated Extractor Code:")
|
|
print("=" * 80)
|
|
print(code)
|
|
|
|
# Save to file
|
|
output_file = Path("generated_extractors") / "cbar_force_extractor.py"
|
|
output_file.parent.mkdir(exist_ok=True)
|
|
with open(output_file, 'w') as f:
|
|
f.write(code)
|
|
|
|
print()
|
|
print(f"[OK] Saved to: {output_file}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|