""" 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.[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]}} ") ''' 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()