feat: Complete Phase 3 - pyNastran Documentation Integration
Phase 3 implements automated OP2 extraction code generation using pyNastran documentation research. This completes the zero-manual-coding pipeline for FEA optimization workflows. Key Features: - PyNastranResearchAgent for automated OP2 code generation - Documentation research via WebFetch integration - 3 core extraction patterns (displacement, stress, force) - Knowledge base architecture for learned patterns - Successfully tested on real OP2 files Phase 2.9 Integration: - Updated HookGenerator with lifecycle hook generation - Added POST_CALCULATION hook point to hooks.py - Created post_calculation/ plugin directory - Generated hooks integrate seamlessly with HookManager New Files: - optimization_engine/pynastran_research_agent.py (600+ lines) - optimization_engine/hook_generator.py (800+ lines) - optimization_engine/inline_code_generator.py - optimization_engine/plugins/post_calculation/ - tests/test_lifecycle_hook_integration.py - docs/SESSION_SUMMARY_PHASE_3.md - docs/SESSION_SUMMARY_PHASE_2_9.md - docs/SESSION_SUMMARY_PHASE_2_8.md - docs/HOOK_ARCHITECTURE.md Modified Files: - README.md - Added Phase 3 completion status - optimization_engine/plugins/hooks.py - Added POST_CALCULATION hook Test Results: - Phase 3 research agent: PASSED - Real OP2 extraction: PASSED (max_disp=0.362mm) - Lifecycle hook integration: PASSED Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
473
optimization_engine/inline_code_generator.py
Normal file
473
optimization_engine/inline_code_generator.py
Normal file
@@ -0,0 +1,473 @@
|
||||
"""
|
||||
Inline Code Generator - Phase 2.8
|
||||
|
||||
Auto-generates simple Python code for mathematical operations that don't require
|
||||
external documentation or research.
|
||||
|
||||
This handles the "inline_calculations" from Phase 2.7 LLM analysis.
|
||||
|
||||
Examples:
|
||||
- Calculate average: avg = sum(values) / len(values)
|
||||
- Find minimum: min_val = min(values)
|
||||
- Normalize: norm_val = value / divisor
|
||||
- Calculate percentage: pct = (value / baseline) * 100
|
||||
|
||||
Author: Atomizer Development Team
|
||||
Version: 0.1.0 (Phase 2.8)
|
||||
Last Updated: 2025-01-16
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeneratedCode:
|
||||
"""Result of code generation."""
|
||||
code: str
|
||||
variables_used: List[str]
|
||||
variables_created: List[str]
|
||||
imports_needed: List[str]
|
||||
description: str
|
||||
|
||||
|
||||
class InlineCodeGenerator:
|
||||
"""
|
||||
Generates Python code for simple mathematical operations.
|
||||
|
||||
This class takes structured calculation descriptions (from LLM Phase 2.7)
|
||||
and generates clean, executable Python code.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the code generator."""
|
||||
self.supported_operations = {
|
||||
'mean', 'average', 'avg',
|
||||
'min', 'minimum',
|
||||
'max', 'maximum',
|
||||
'sum', 'total',
|
||||
'count', 'length',
|
||||
'normalize', 'norm',
|
||||
'percentage', 'percent', 'pct',
|
||||
'ratio',
|
||||
'difference', 'diff',
|
||||
'add', 'subtract', 'multiply', 'divide',
|
||||
'abs', 'absolute',
|
||||
'sqrt', 'square_root',
|
||||
'power', 'pow'
|
||||
}
|
||||
|
||||
def generate_from_llm_output(self, calculation: Dict[str, Any]) -> GeneratedCode:
|
||||
"""
|
||||
Generate code from LLM-analyzed calculation.
|
||||
|
||||
Args:
|
||||
calculation: Dictionary from LLM with keys:
|
||||
- action: str (e.g., "calculate_average")
|
||||
- description: str
|
||||
- params: dict with input/operation/etc.
|
||||
- code_hint: str (optional, from LLM)
|
||||
|
||||
Returns:
|
||||
GeneratedCode with executable Python code
|
||||
"""
|
||||
action = calculation.get('action', '')
|
||||
params = calculation.get('params', {})
|
||||
description = calculation.get('description', '')
|
||||
code_hint = calculation.get('code_hint', '')
|
||||
|
||||
# If LLM provided a code hint, validate and use it
|
||||
if code_hint:
|
||||
return self._from_code_hint(code_hint, params, description)
|
||||
|
||||
# Otherwise, generate from action/params
|
||||
return self._from_action_params(action, params, description)
|
||||
|
||||
def _from_code_hint(self, code_hint: str, params: Dict[str, Any],
|
||||
description: str) -> GeneratedCode:
|
||||
"""Generate from LLM-provided code hint."""
|
||||
# Extract variable names from code hint
|
||||
variables_used = self._extract_input_variables(code_hint, params)
|
||||
variables_created = self._extract_output_variables(code_hint)
|
||||
imports_needed = self._extract_imports_needed(code_hint)
|
||||
|
||||
return GeneratedCode(
|
||||
code=code_hint.strip(),
|
||||
variables_used=variables_used,
|
||||
variables_created=variables_created,
|
||||
imports_needed=imports_needed,
|
||||
description=description
|
||||
)
|
||||
|
||||
def _from_action_params(self, action: str, params: Dict[str, Any],
|
||||
description: str) -> GeneratedCode:
|
||||
"""Generate code from action name and parameters."""
|
||||
operation = params.get('operation', '').lower()
|
||||
input_var = params.get('input', 'values')
|
||||
divisor = params.get('divisor')
|
||||
baseline = params.get('baseline')
|
||||
current = params.get('current')
|
||||
|
||||
# Detect operation type
|
||||
if any(op in action.lower() or op in operation for op in ['avg', 'average', 'mean']):
|
||||
return self._generate_average(input_var, description)
|
||||
|
||||
elif any(op in action.lower() or op in operation for op in ['min', 'minimum']):
|
||||
return self._generate_min(input_var, description)
|
||||
|
||||
elif any(op in action.lower() or op in operation for op in ['max', 'maximum']):
|
||||
return self._generate_max(input_var, description)
|
||||
|
||||
elif any(op in action.lower() for op in ['normalize', 'norm']) and divisor:
|
||||
return self._generate_normalization(input_var, divisor, description)
|
||||
|
||||
elif any(op in action.lower() for op in ['percentage', 'percent', 'pct', 'increase']):
|
||||
current = params.get('current')
|
||||
baseline = params.get('baseline')
|
||||
if current and baseline:
|
||||
return self._generate_percentage_change(current, baseline, description)
|
||||
elif divisor:
|
||||
return self._generate_percentage(input_var, divisor, description)
|
||||
|
||||
elif 'sum' in action.lower() or 'total' in action.lower():
|
||||
return self._generate_sum(input_var, description)
|
||||
|
||||
elif 'ratio' in action.lower():
|
||||
inputs = params.get('inputs', [])
|
||||
if len(inputs) >= 2:
|
||||
return self._generate_ratio(inputs[0], inputs[1], description)
|
||||
|
||||
# Fallback: generic operation
|
||||
return self._generate_generic(action, params, description)
|
||||
|
||||
def _generate_average(self, input_var: str, description: str) -> GeneratedCode:
|
||||
"""Generate code to calculate average."""
|
||||
output_var = f"avg_{input_var}" if not input_var.startswith('avg') else input_var.replace('input', 'avg')
|
||||
code = f"{output_var} = sum({input_var}) / len({input_var})"
|
||||
|
||||
return GeneratedCode(
|
||||
code=code,
|
||||
variables_used=[input_var],
|
||||
variables_created=[output_var],
|
||||
imports_needed=[],
|
||||
description=description or f"Calculate average of {input_var}"
|
||||
)
|
||||
|
||||
def _generate_min(self, input_var: str, description: str) -> GeneratedCode:
|
||||
"""Generate code to find minimum."""
|
||||
output_var = f"min_{input_var}" if not input_var.startswith('min') else input_var.replace('input', 'min')
|
||||
code = f"{output_var} = min({input_var})"
|
||||
|
||||
return GeneratedCode(
|
||||
code=code,
|
||||
variables_used=[input_var],
|
||||
variables_created=[output_var],
|
||||
imports_needed=[],
|
||||
description=description or f"Find minimum of {input_var}"
|
||||
)
|
||||
|
||||
def _generate_max(self, input_var: str, description: str) -> GeneratedCode:
|
||||
"""Generate code to find maximum."""
|
||||
output_var = f"max_{input_var}" if not input_var.startswith('max') else input_var.replace('input', 'max')
|
||||
code = f"{output_var} = max({input_var})"
|
||||
|
||||
return GeneratedCode(
|
||||
code=code,
|
||||
variables_used=[input_var],
|
||||
variables_created=[output_var],
|
||||
imports_needed=[],
|
||||
description=description or f"Find maximum of {input_var}"
|
||||
)
|
||||
|
||||
def _generate_normalization(self, input_var: str, divisor: float,
|
||||
description: str) -> GeneratedCode:
|
||||
"""Generate code to normalize a value."""
|
||||
output_var = f"norm_{input_var}" if not input_var.startswith('norm') else input_var
|
||||
code = f"{output_var} = {input_var} / {divisor}"
|
||||
|
||||
return GeneratedCode(
|
||||
code=code,
|
||||
variables_used=[input_var],
|
||||
variables_created=[output_var],
|
||||
imports_needed=[],
|
||||
description=description or f"Normalize {input_var} by {divisor}"
|
||||
)
|
||||
|
||||
def _generate_percentage_change(self, current: str, baseline: str,
|
||||
description: str) -> GeneratedCode:
|
||||
"""Generate code to calculate percentage change."""
|
||||
# Infer output variable name from inputs
|
||||
if 'mass' in current.lower() or 'mass' in baseline.lower():
|
||||
output_var = "mass_increase_pct"
|
||||
else:
|
||||
output_var = f"{current}_vs_{baseline}_pct"
|
||||
|
||||
code = f"{output_var} = (({current} - {baseline}) / {baseline}) * 100.0"
|
||||
|
||||
return GeneratedCode(
|
||||
code=code,
|
||||
variables_used=[current, baseline],
|
||||
variables_created=[output_var],
|
||||
imports_needed=[],
|
||||
description=description or f"Calculate percentage change from {baseline} to {current}"
|
||||
)
|
||||
|
||||
def _generate_percentage(self, input_var: str, divisor: float,
|
||||
description: str) -> GeneratedCode:
|
||||
"""Generate code to calculate percentage."""
|
||||
output_var = f"pct_{input_var}"
|
||||
code = f"{output_var} = ({input_var} / {divisor}) * 100.0"
|
||||
|
||||
return GeneratedCode(
|
||||
code=code,
|
||||
variables_used=[input_var],
|
||||
variables_created=[output_var],
|
||||
imports_needed=[],
|
||||
description=description or f"Calculate percentage of {input_var} vs {divisor}"
|
||||
)
|
||||
|
||||
def _generate_sum(self, input_var: str, description: str) -> GeneratedCode:
|
||||
"""Generate code to calculate sum."""
|
||||
output_var = f"total_{input_var}" if not input_var.startswith('total') else input_var
|
||||
code = f"{output_var} = sum({input_var})"
|
||||
|
||||
return GeneratedCode(
|
||||
code=code,
|
||||
variables_used=[input_var],
|
||||
variables_created=[output_var],
|
||||
imports_needed=[],
|
||||
description=description or f"Calculate sum of {input_var}"
|
||||
)
|
||||
|
||||
def _generate_ratio(self, numerator: str, denominator: str,
|
||||
description: str) -> GeneratedCode:
|
||||
"""Generate code to calculate ratio."""
|
||||
output_var = f"{numerator}_to_{denominator}_ratio"
|
||||
code = f"{output_var} = {numerator} / {denominator}"
|
||||
|
||||
return GeneratedCode(
|
||||
code=code,
|
||||
variables_used=[numerator, denominator],
|
||||
variables_created=[output_var],
|
||||
imports_needed=[],
|
||||
description=description or f"Calculate ratio of {numerator} to {denominator}"
|
||||
)
|
||||
|
||||
def _generate_generic(self, action: str, params: Dict[str, Any],
|
||||
description: str) -> GeneratedCode:
|
||||
"""Generate generic calculation code."""
|
||||
# Extract operation from action name
|
||||
operation = action.lower().replace('calculate_', '').replace('find_', '').replace('get_', '')
|
||||
input_var = params.get('input', 'value')
|
||||
output_var = f"{operation}_result"
|
||||
|
||||
# Try to infer code from parameters
|
||||
if 'formula' in params:
|
||||
code = f"{output_var} = {params['formula']}"
|
||||
else:
|
||||
code = f"{output_var} = {input_var} # TODO: Implement {action}"
|
||||
|
||||
return GeneratedCode(
|
||||
code=code,
|
||||
variables_used=[input_var] if input_var != 'value' else [],
|
||||
variables_created=[output_var],
|
||||
imports_needed=[],
|
||||
description=description or f"Generic calculation: {action}"
|
||||
)
|
||||
|
||||
def _extract_input_variables(self, code: str, params: Dict[str, Any]) -> List[str]:
|
||||
"""Extract input variable names from code."""
|
||||
variables = []
|
||||
|
||||
# Get from params if available
|
||||
if 'input' in params:
|
||||
variables.append(params['input'])
|
||||
if 'inputs' in params:
|
||||
variables.extend(params.get('inputs', []))
|
||||
if 'current' in params:
|
||||
variables.append(params['current'])
|
||||
if 'baseline' in params:
|
||||
variables.append(params['baseline'])
|
||||
|
||||
# Extract from code (variables on right side of =)
|
||||
if '=' in code:
|
||||
rhs = code.split('=', 1)[1]
|
||||
# Simple extraction of variable names (alphanumeric + underscore)
|
||||
import re
|
||||
found_vars = re.findall(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', rhs)
|
||||
variables.extend([v for v in found_vars if v not in ['sum', 'min', 'max', 'len', 'abs']])
|
||||
|
||||
return list(set(variables)) # Remove duplicates
|
||||
|
||||
def _extract_output_variables(self, code: str) -> List[str]:
|
||||
"""Extract output variable names from code."""
|
||||
# Variables on left side of =
|
||||
if '=' in code:
|
||||
lhs = code.split('=', 1)[0].strip()
|
||||
return [lhs]
|
||||
return []
|
||||
|
||||
def _extract_imports_needed(self, code: str) -> List[str]:
|
||||
"""Extract required imports from code."""
|
||||
imports = []
|
||||
|
||||
# Check for math functions
|
||||
if any(func in code for func in ['sqrt', 'pow', 'log', 'exp', 'sin', 'cos']):
|
||||
imports.append('import math')
|
||||
|
||||
# Check for numpy functions
|
||||
if any(func in code for func in ['np.', 'numpy.']):
|
||||
imports.append('import numpy as np')
|
||||
|
||||
return imports
|
||||
|
||||
def generate_batch(self, calculations: List[Dict[str, Any]]) -> List[GeneratedCode]:
|
||||
"""
|
||||
Generate code for multiple calculations.
|
||||
|
||||
Args:
|
||||
calculations: List of calculation dictionaries from LLM
|
||||
|
||||
Returns:
|
||||
List of GeneratedCode objects
|
||||
"""
|
||||
return [self.generate_from_llm_output(calc) for calc in calculations]
|
||||
|
||||
def generate_executable_script(self, calculations: List[Dict[str, Any]],
|
||||
inputs: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
Generate a complete executable Python script with all calculations.
|
||||
|
||||
Args:
|
||||
calculations: List of calculations
|
||||
inputs: Optional input values for testing
|
||||
|
||||
Returns:
|
||||
Complete Python script as string
|
||||
"""
|
||||
generated = self.generate_batch(calculations)
|
||||
|
||||
# Collect all imports
|
||||
all_imports = []
|
||||
for code in generated:
|
||||
all_imports.extend(code.imports_needed)
|
||||
all_imports = list(set(all_imports)) # Remove duplicates
|
||||
|
||||
# Build script
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append('"""')
|
||||
lines.append('Auto-generated inline calculations')
|
||||
lines.append('Generated by Atomizer Phase 2.8 Inline Code Generator')
|
||||
lines.append('"""')
|
||||
lines.append('')
|
||||
|
||||
# Imports
|
||||
if all_imports:
|
||||
lines.extend(all_imports)
|
||||
lines.append('')
|
||||
|
||||
# Input values (if provided for testing)
|
||||
if inputs:
|
||||
lines.append('# Input values')
|
||||
for var_name, value in inputs.items():
|
||||
lines.append(f'{var_name} = {repr(value)}')
|
||||
lines.append('')
|
||||
|
||||
# Calculations
|
||||
lines.append('# Inline calculations')
|
||||
for code_obj in generated:
|
||||
lines.append(f'# {code_obj.description}')
|
||||
lines.append(code_obj.code)
|
||||
lines.append('')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
"""Test the inline code generator."""
|
||||
print("=" * 80)
|
||||
print("Phase 2.8: Inline Code Generator Test")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
generator = InlineCodeGenerator()
|
||||
|
||||
# Test cases from Phase 2.7 LLM output
|
||||
test_calculations = [
|
||||
{
|
||||
"action": "normalize_stress",
|
||||
"description": "Normalize stress by 200 MPa",
|
||||
"params": {
|
||||
"input": "max_stress",
|
||||
"divisor": 200.0,
|
||||
"units": "MPa"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "normalize_displacement",
|
||||
"description": "Normalize displacement by 5 mm",
|
||||
"params": {
|
||||
"input": "max_disp_y",
|
||||
"divisor": 5.0,
|
||||
"units": "mm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "calculate_mass_increase",
|
||||
"description": "Calculate mass increase percentage vs baseline",
|
||||
"params": {
|
||||
"current": "panel_total_mass",
|
||||
"baseline": "baseline_mass"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "calculate_average",
|
||||
"description": "Calculate average of extracted forces",
|
||||
"params": {
|
||||
"input": "forces_z",
|
||||
"operation": "mean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "find_minimum",
|
||||
"description": "Find minimum force value",
|
||||
"params": {
|
||||
"input": "forces_z",
|
||||
"operation": "min"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
print("Test Calculations:")
|
||||
print()
|
||||
|
||||
for i, calc in enumerate(test_calculations, 1):
|
||||
print(f"{i}. {calc['description']}")
|
||||
code_obj = generator.generate_from_llm_output(calc)
|
||||
print(f" Generated Code: {code_obj.code}")
|
||||
print(f" Inputs: {', '.join(code_obj.variables_used)}")
|
||||
print(f" Outputs: {', '.join(code_obj.variables_created)}")
|
||||
print()
|
||||
|
||||
# Generate complete script
|
||||
print("=" * 80)
|
||||
print("Complete Executable Script:")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
test_inputs = {
|
||||
'max_stress': 150.5,
|
||||
'max_disp_y': 3.2,
|
||||
'panel_total_mass': 2.8,
|
||||
'baseline_mass': 2.5,
|
||||
'forces_z': [10.5, 12.3, 8.9, 11.2, 9.8]
|
||||
}
|
||||
|
||||
script = generator.generate_executable_script(test_calculations, test_inputs)
|
||||
print(script)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user