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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user