Files
Atomizer/optimization_engine/future/pynastran_research_agent.py

403 lines
13 KiB
Python
Raw Normal View History

2025-11-16 16:33:48 -05:00
"""
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
feat: Add substudy system with live history tracking and workflow fixes Major Features: - Hierarchical substudy system (like NX Solutions/Subcases) * Shared model files across all substudies * Independent configuration per substudy * Continuation support from previous substudies * Real-time incremental history updates - Live history tracking with optimization_history_incremental.json - Complete bracket_displacement_maximizing study with substudy examples Core Fixes: - Fixed expression update workflow to pass design_vars through simulation_runner * Restored working NX journal expression update mechanism * OP2 timestamp verification instead of file deletion * Resolved issue where all trials returned identical objective values - Fixed LLMOptimizationRunner to pass design variables to simulation runner - Enhanced NXSolver with timestamp-based file regeneration verification New Components: - optimization_engine/llm_optimization_runner.py - LLM-driven optimization runner - optimization_engine/optimization_setup_wizard.py - Phase 3.3 setup wizard - studies/bracket_displacement_maximizing/ - Complete substudy example * run_substudy.py - Substudy runner with continuation * run_optimization.py - Standalone optimization runner * config/substudy_template.json - Template for new substudies * substudies/coarse_exploration/ - 20-trial coarse search * substudies/fine_tuning/ - 50-trial refinement (continuation example) * SUBSTUDIES_README.md - Complete substudy documentation Technical Improvements: - Incremental history saving after each trial (optimization_history_incremental.json) - Expression update workflow: .prt update → NX journal receives values → geometry update → FEM update → solve - Trial indexing fix in substudy result saving - Updated README with substudy system documentation Testing: - Successfully ran 20-trial coarse_exploration substudy - Verified different objective values across trials (workflow fix validated) - Confirmed live history updates in real-time - Tested shared model file usage across substudies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 21:29:54 -05:00
# In pyNastran, stress is stored in model.op2_results.stress
2025-11-16 16:33:48 -05:00
stress_attr = f"{element_type}_stress"
feat: Add substudy system with live history tracking and workflow fixes Major Features: - Hierarchical substudy system (like NX Solutions/Subcases) * Shared model files across all substudies * Independent configuration per substudy * Continuation support from previous substudies * Real-time incremental history updates - Live history tracking with optimization_history_incremental.json - Complete bracket_displacement_maximizing study with substudy examples Core Fixes: - Fixed expression update workflow to pass design_vars through simulation_runner * Restored working NX journal expression update mechanism * OP2 timestamp verification instead of file deletion * Resolved issue where all trials returned identical objective values - Fixed LLMOptimizationRunner to pass design variables to simulation runner - Enhanced NXSolver with timestamp-based file regeneration verification New Components: - optimization_engine/llm_optimization_runner.py - LLM-driven optimization runner - optimization_engine/optimization_setup_wizard.py - Phase 3.3 setup wizard - studies/bracket_displacement_maximizing/ - Complete substudy example * run_substudy.py - Substudy runner with continuation * run_optimization.py - Standalone optimization runner * config/substudy_template.json - Template for new substudies * substudies/coarse_exploration/ - 20-trial coarse search * substudies/fine_tuning/ - 50-trial refinement (continuation example) * SUBSTUDIES_README.md - Complete substudy documentation Technical Improvements: - Incremental history saving after each trial (optimization_history_incremental.json) - Expression update workflow: .prt update → NX journal receives values → geometry update → FEM update → solve - Trial indexing fix in substudy result saving - Updated README with substudy system documentation Testing: - Successfully ran 20-trial coarse_exploration substudy - Verified different objective values across trials (workflow fix validated) - Confirmed live history updates in real-time - Tested shared model file usage across substudies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 21:29:54 -05:00
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):
2025-11-16 16:33:48 -05:00
raise ValueError(f"No {element_type} stress results in OP2")
feat: Add substudy system with live history tracking and workflow fixes Major Features: - Hierarchical substudy system (like NX Solutions/Subcases) * Shared model files across all substudies * Independent configuration per substudy * Continuation support from previous substudies * Real-time incremental history updates - Live history tracking with optimization_history_incremental.json - Complete bracket_displacement_maximizing study with substudy examples Core Fixes: - Fixed expression update workflow to pass design_vars through simulation_runner * Restored working NX journal expression update mechanism * OP2 timestamp verification instead of file deletion * Resolved issue where all trials returned identical objective values - Fixed LLMOptimizationRunner to pass design variables to simulation runner - Enhanced NXSolver with timestamp-based file regeneration verification New Components: - optimization_engine/llm_optimization_runner.py - LLM-driven optimization runner - optimization_engine/optimization_setup_wizard.py - Phase 3.3 setup wizard - studies/bracket_displacement_maximizing/ - Complete substudy example * run_substudy.py - Substudy runner with continuation * run_optimization.py - Standalone optimization runner * config/substudy_template.json - Template for new substudies * substudies/coarse_exploration/ - 20-trial coarse search * substudies/fine_tuning/ - 50-trial refinement (continuation example) * SUBSTUDIES_README.md - Complete substudy documentation Technical Improvements: - Incremental history saving after each trial (optimization_history_incremental.json) - Expression update workflow: .prt update → NX journal receives values → geometry update → FEM update → solve - Trial indexing fix in substudy result saving - Updated README with substudy system documentation Testing: - Successfully ran 20-trial coarse_exploration substudy - Verified different objective values across trials (workflow fix validated) - Confirmed live history updates in real-time - Tested shared model file usage across substudies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 21:29:54 -05:00
stress = getattr(stress_obj, stress_attr)[subcase]
2025-11-16 16:33:48 -05:00
itime = 0
# Extract von Mises if available
feat: Add substudy system with live history tracking and workflow fixes Major Features: - Hierarchical substudy system (like NX Solutions/Subcases) * Shared model files across all substudies * Independent configuration per substudy * Continuation support from previous substudies * Real-time incremental history updates - Live history tracking with optimization_history_incremental.json - Complete bracket_displacement_maximizing study with substudy examples Core Fixes: - Fixed expression update workflow to pass design_vars through simulation_runner * Restored working NX journal expression update mechanism * OP2 timestamp verification instead of file deletion * Resolved issue where all trials returned identical objective values - Fixed LLMOptimizationRunner to pass design variables to simulation runner - Enhanced NXSolver with timestamp-based file regeneration verification New Components: - optimization_engine/llm_optimization_runner.py - LLM-driven optimization runner - optimization_engine/optimization_setup_wizard.py - Phase 3.3 setup wizard - studies/bracket_displacement_maximizing/ - Complete substudy example * run_substudy.py - Substudy runner with continuation * run_optimization.py - Standalone optimization runner * config/substudy_template.json - Template for new substudies * substudies/coarse_exploration/ - 20-trial coarse search * substudies/fine_tuning/ - 50-trial refinement (continuation example) * SUBSTUDIES_README.md - Complete substudy documentation Technical Improvements: - Incremental history saving after each trial (optimization_history_incremental.json) - Expression update workflow: .prt update → NX journal receives values → geometry update → FEM update → solve - Trial indexing fix in substudy result saving - Updated README with substudy system documentation Testing: - Successfully ran 20-trial coarse_exploration substudy - Verified different objective values across trials (workflow fix validated) - Confirmed live history updates in real-time - Tested shared model file usage across substudies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 21:29:54 -05:00
if stress.is_von_mises: # Property, not method
2025-11-16 16:33:48 -05:00
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()