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

@@ -0,0 +1,75 @@
"""
Comparison Hook
Auto-generated by Atomizer Phase 2.9
Compare min force to average
Operation: ratio
Formula: min_to_avg_ratio = min_force / avg_force
"""
import sys
import json
from pathlib import Path
def compare_ratio(min_force, avg_force):
"""
Compare values using ratio.
Args:
min_force: float
avg_force: float
Returns:
float: Comparison result
"""
result = min_force / avg_force
return result
def main():
"""Main entry point for hook execution."""
if len(sys.argv) < 2:
print("Usage: python {} <input_file.json>".format(sys.argv[0]))
sys.exit(1)
input_file = Path(sys.argv[1])
# Read inputs
with open(input_file, 'r') as f:
inputs = json.load(f)
# Extract required inputs
min_force = inputs.get("min_force")
if min_force is None:
print(f"Error: Required input 'min_force' not found")
sys.exit(1)
avg_force = inputs.get("avg_force")
if avg_force is None:
print(f"Error: Required input 'avg_force' not found")
sys.exit(1)
# Calculate comparison
result = compare_ratio(min_force, avg_force)
# Write output
output_file = input_file.parent / "min_to_avg_ratio.json"
output = {
"min_to_avg_ratio": result,
"operation": "ratio",
"formula": "min_force / avg_force",
"inputs_used": {"min_force": min_force, "avg_force": avg_force}
}
with open(output_file, 'w') as f:
json.dump(output, f, indent=2)
print(f"min_to_avg_ratio = {result:.6f}")
print(f"Result saved to: {output_file}")
return result
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,83 @@
"""
Constraint Check Hook
Auto-generated by Atomizer Phase 2.9
Check if stress is below yield
Constraint: max_stress / yield_strength
Threshold: 1.0
"""
import sys
import json
from pathlib import Path
def check_yield_constraint(max_stress, yield_strength):
"""
Check constraint condition.
Args:
max_stress: float
yield_strength: float
Returns:
tuple: (satisfied: bool, value: float, violation: float)
"""
value = max_stress / yield_strength
satisfied = value <= 1.0
violation = max(0.0, value - 1.0)
return satisfied, value, violation
def main():
"""Main entry point for hook execution."""
if len(sys.argv) < 2:
print("Usage: python {} <input_file.json>".format(sys.argv[0]))
sys.exit(1)
input_file = Path(sys.argv[1])
# Read inputs
with open(input_file, 'r') as f:
inputs = json.load(f)
# Extract required inputs
max_stress = inputs.get("max_stress")
if max_stress is None:
print(f"Error: Required input 'max_stress' not found")
sys.exit(1)
yield_strength = inputs.get("yield_strength")
if yield_strength is None:
print(f"Error: Required input 'yield_strength' not found")
sys.exit(1)
# Check constraint
satisfied, value, violation = check_yield_constraint(max_stress, yield_strength)
# Write output
output_file = input_file.parent / "yield_constraint_check.json"
output = {
"constraint_name": "yield_constraint",
"satisfied": satisfied,
"value": value,
"threshold": 1.0,
"violation": violation,
"inputs_used": {"max_stress": max_stress, "yield_strength": yield_strength}
}
with open(output_file, 'w') as f:
json.dump(output, f, indent=2)
status = "SATISFIED" if satisfied else "VIOLATED"
print(f"Constraint {status}: {value:.6f} (threshold: 1.0)")
if not satisfied:
print(f"Violation: {violation:.6f}")
print(f"Result saved to: {output_file}")
return value
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,74 @@
"""
Custom Formula Hook
Auto-generated by Atomizer Phase 2.9
Calculate safety factor
Formula: safety_factor = yield_strength / max_stress
Inputs: max_stress, yield_strength
"""
import sys
import json
from pathlib import Path
def calculate_safety_factor(max_stress, yield_strength):
"""
Calculate custom metric using formula.
Args:
max_stress: float
yield_strength: float
Returns:
float: safety_factor
"""
safety_factor = yield_strength / max_stress
return safety_factor
def main():
"""Main entry point for hook execution."""
if len(sys.argv) < 2:
print("Usage: python {} <input_file.json>".format(sys.argv[0]))
sys.exit(1)
input_file = Path(sys.argv[1])
# Read inputs
with open(input_file, 'r') as f:
inputs = json.load(f)
# Extract required inputs
max_stress = inputs.get("max_stress")
if max_stress is None:
print(f"Error: Required input 'max_stress' not found")
sys.exit(1)
yield_strength = inputs.get("yield_strength")
if yield_strength is None:
print(f"Error: Required input 'yield_strength' not found")
sys.exit(1)
# Calculate result
result = calculate_safety_factor(max_stress, yield_strength)
# Write output
output_file = input_file.parent / "safety_factor_result.json"
output = {
"safety_factor": result,
"formula": "yield_strength / max_stress",
"inputs_used": {"max_stress": max_stress, "yield_strength": yield_strength}
}
with open(output_file, 'w') as f:
json.dump(output, f, indent=2)
print(f"safety_factor = {result:.6f}")
print(f"Result saved to: {output_file}")
return result
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,54 @@
{
"hooks": [
{
"name": "hook_weighted_objective_norm_stress_norm_disp.py",
"type": "weighted_objective",
"description": "Combine normalized stress (70%) and displacement (30%)",
"inputs": [
"norm_stress",
"norm_disp"
],
"outputs": [
"weighted_objective"
]
},
{
"name": "hook_custom_safety_factor.py",
"type": "custom_formula",
"description": "Calculate safety factor",
"inputs": [
"max_stress",
"yield_strength"
],
"outputs": [
"safety_factor"
]
},
{
"name": "hook_compare_min_to_avg_ratio.py",
"type": "comparison",
"description": "Compare min force to average",
"inputs": [
"min_force",
"avg_force"
],
"outputs": [
"min_to_avg_ratio"
]
},
{
"name": "hook_constraint_yield_constraint.py",
"type": "constraint_check",
"description": "Check if stress is below yield",
"inputs": [
"max_stress",
"yield_strength"
],
"outputs": [
"yield_constraint",
"yield_constraint_satisfied",
"yield_constraint_violation"
]
}
]
}

View File

@@ -0,0 +1,85 @@
"""
Weighted Objective Function Hook
Auto-generated by Atomizer Phase 2.9
Combine normalized stress (70%) and displacement (30%)
Inputs: norm_stress, norm_disp
Weights: 0.7, 0.3
Formula: 0.7 * norm_stress + 0.3 * norm_disp
Objective: minimize
"""
import sys
import json
from pathlib import Path
def weighted_objective(norm_stress, norm_disp):
"""
Calculate weighted objective from multiple inputs.
Args:
norm_stress: float
norm_disp: float
Returns:
float: Weighted objective value
"""
result = 0.7 * norm_stress + 0.3 * norm_disp
return result
def main():
"""
Main entry point for hook execution.
Reads inputs from JSON file, calculates objective, writes output.
"""
# Parse command line arguments
if len(sys.argv) < 2:
print("Usage: python {} <input_file.json>".format(sys.argv[0]))
sys.exit(1)
input_file = Path(sys.argv[1])
# Read inputs
if not input_file.exists():
print(f"Error: Input file {input_file} not found")
sys.exit(1)
with open(input_file, 'r') as f:
inputs = json.load(f)
# Extract required inputs
norm_stress = inputs.get("norm_stress")
if norm_stress is None:
print(f"Error: Required input 'norm_stress' not found")
sys.exit(1)
norm_disp = inputs.get("norm_disp")
if norm_disp is None:
print(f"Error: Required input 'norm_disp' not found")
sys.exit(1)
# Calculate weighted objective
result = weighted_objective(norm_stress, norm_disp)
# Write output
output_file = input_file.parent / "weighted_objective_result.json"
output = {
"weighted_objective": result,
"objective_type": "minimize",
"inputs_used": {"norm_stress": norm_stress, "norm_disp": norm_disp},
"formula": "0.7 * norm_stress + 0.3 * norm_disp"
}
with open(output_file, 'w') as f:
json.dump(output, f, indent=2)
print(f"Weighted objective calculated: {result:.6f}")
print(f"Result saved to: {output_file}")
return result
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,4 @@
{
"norm_stress": 0.75,
"norm_disp": 0.64
}

View File

@@ -0,0 +1,9 @@
{
"weighted_objective": 0.7169999999999999,
"objective_type": "minimize",
"inputs_used": {
"norm_stress": 0.75,
"norm_disp": 0.64
},
"formula": "0.7 * norm_stress + 0.3 * norm_disp"
}