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:
2025-11-16 16:33:48 -05:00
parent 0a7cca9c6a
commit 38abb0d8d2
23 changed files with 4939 additions and 6 deletions

View File

@@ -20,6 +20,7 @@ class HookPoint(Enum):
PRE_SOLVE = "pre_solve" # Before solver execution
POST_SOLVE = "post_solve" # After solve, before extraction
POST_EXTRACTION = "post_extraction" # After result extraction
POST_CALCULATION = "post_calculation" # After inline calculations (Phase 2.8), before reporting
CUSTOM_OBJECTIVE = "custom_objective" # Custom objective functions

View File

@@ -0,0 +1,72 @@
"""
Compare min force to average
Auto-generated lifecycle hook by Atomizer Phase 2.9
Hook Point: post_calculation
Inputs: min_force, avg_force
Outputs: min_to_avg_ratio
"""
import logging
from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
def ratio_hook(context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Compare min force to average
Args:
context: Hook context containing:
- trial_number: Current optimization trial
- results: Dictionary with extracted FEA results
- calculations: Dictionary with inline calculation results
Returns:
Dictionary with calculated values to add to context
"""
logger.info(f"Executing ratio_hook for trial {context.get('trial_number', 'unknown')}")
# Extract inputs from context
results = context.get('results', {})
calculations = context.get('calculations', {})
min_force = calculations.get('min_force') or results.get('min_force')
if min_force is None:
logger.error(f"Required input 'min_force' not found in context")
raise ValueError(f"Missing required input: min_force")
avg_force = calculations.get('avg_force') or results.get('avg_force')
if avg_force is None:
logger.error(f"Required input 'avg_force' not found in context")
raise ValueError(f"Missing required input: avg_force")
# Calculate comparison
result = min_force / avg_force
logger.info(f"min_to_avg_ratio = {result:.6f}")
return {
'min_to_avg_ratio': result
}
def register_hooks(hook_manager):
"""
Register this hook with the HookManager.
This function is called automatically when the plugin is loaded.
Args:
hook_manager: The HookManager instance
"""
hook_manager.register_hook(
hook_point='post_calculation',
function=ratio_hook,
description="Compare min force to average",
name="ratio_hook",
priority=100,
enabled=True
)
logger.info(f"Registered ratio_hook at post_calculation")

View File

@@ -0,0 +1,72 @@
"""
Calculate safety factor
Auto-generated lifecycle hook by Atomizer Phase 2.9
Hook Point: post_calculation
Inputs: max_stress, yield_strength
Outputs: safety_factor
"""
import logging
from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
def safety_factor_hook(context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Calculate safety factor
Args:
context: Hook context containing:
- trial_number: Current optimization trial
- results: Dictionary with extracted FEA results
- calculations: Dictionary with inline calculation results
Returns:
Dictionary with calculated values to add to context
"""
logger.info(f"Executing safety_factor_hook for trial {context.get('trial_number', 'unknown')}")
# Extract inputs from context
results = context.get('results', {})
calculations = context.get('calculations', {})
max_stress = calculations.get('max_stress') or results.get('max_stress')
if max_stress is None:
logger.error(f"Required input 'max_stress' not found in context")
raise ValueError(f"Missing required input: max_stress")
yield_strength = calculations.get('yield_strength') or results.get('yield_strength')
if yield_strength is None:
logger.error(f"Required input 'yield_strength' not found in context")
raise ValueError(f"Missing required input: yield_strength")
# Calculate using custom formula
safety_factor = yield_strength / max_stress
logger.info(f"safety_factor = {safety_factor:.6f}")
return {
'safety_factor': safety_factor
}
def register_hooks(hook_manager):
"""
Register this hook with the HookManager.
This function is called automatically when the plugin is loaded.
Args:
hook_manager: The HookManager instance
"""
hook_manager.register_hook(
hook_point='post_calculation',
function=safety_factor_hook,
description="Calculate safety factor",
name="safety_factor_hook",
priority=100,
enabled=True
)
logger.info(f"Registered safety_factor_hook at post_calculation")

View File

@@ -0,0 +1,73 @@
"""
Combine normalized stress (70%) and displacement (30%)
Auto-generated lifecycle hook by Atomizer Phase 2.9
Hook Point: post_calculation
Inputs: norm_stress, norm_disp
Outputs: weighted_objective
"""
import logging
from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
def weighted_objective_hook(context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Combine normalized stress (70%) and displacement (30%)
Args:
context: Hook context containing:
- trial_number: Current optimization trial
- results: Dictionary with extracted FEA results
- calculations: Dictionary with inline calculation results
Returns:
Dictionary with calculated values to add to context
"""
logger.info(f"Executing weighted_objective_hook for trial {context.get('trial_number', 'unknown')}")
# Extract inputs from context
results = context.get('results', {})
calculations = context.get('calculations', {})
norm_stress = calculations.get('norm_stress') or results.get('norm_stress')
if norm_stress is None:
logger.error(f"Required input 'norm_stress' not found in context")
raise ValueError(f"Missing required input: norm_stress")
norm_disp = calculations.get('norm_disp') or results.get('norm_disp')
if norm_disp is None:
logger.error(f"Required input 'norm_disp' not found in context")
raise ValueError(f"Missing required input: norm_disp")
# Calculate weighted objective
result = 0.7 * norm_stress + 0.3 * norm_disp
logger.info(f"Weighted objective calculated: {result:.6f}")
return {
'weighted_objective': result,
'weighted_objective': result
}
def register_hooks(hook_manager):
"""
Register this hook with the HookManager.
This function is called automatically when the plugin is loaded.
Args:
hook_manager: The HookManager instance
"""
hook_manager.register_hook(
hook_point='post_calculation',
function=weighted_objective_hook,
description="Combine normalized stress (70%) and displacement (30%)",
name="weighted_objective_hook",
priority=100,
enabled=True
)
logger.info(f"Registered weighted_objective_hook at post_calculation")