feat: Implement Phase 1 - Plugin & Hook System

Core plugin architecture for LLM-driven optimization:

New Features:
- Hook system with 6 lifecycle points (pre_mesh, post_mesh, pre_solve, post_solve, post_extraction, custom_objectives)
- HookManager for centralized registration and execution
- Code validation with AST-based safety checks
- Feature registry (JSON) for LLM capability discovery
- Example plugin: log_trial_start
- 23 comprehensive tests (all passing)

Integration:
- OptimizationRunner now loads plugins automatically
- Hooks execute at 5 points in optimization loop
- Custom objectives can override total_objective via hooks

Safety:
- Module whitelist (numpy, scipy, pandas, optuna, pyNastran)
- Dangerous operation blocking (eval, exec, os.system, subprocess)
- Optional file operation permission flag

Files Added:
- optimization_engine/plugins/__init__.py
- optimization_engine/plugins/hooks.py
- optimization_engine/plugins/hook_manager.py
- optimization_engine/plugins/validators.py
- optimization_engine/feature_registry.json
- optimization_engine/plugins/pre_solve/log_trial_start.py
- tests/test_plugin_system.py (23 tests)

Files Modified:
- optimization_engine/runner.py (added hook integration)

Ready for Phase 2: LLM interface layer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-15 14:46:49 -05:00
parent 0ce9ddf3e2
commit a24e3f750c
14 changed files with 1473 additions and 0 deletions

View File

@@ -23,6 +23,8 @@ import pandas as pd
from datetime import datetime
import pickle
from optimization_engine.plugins import HookManager
class OptimizationRunner:
"""
@@ -68,6 +70,15 @@ class OptimizationRunner:
self.output_dir = self.config_path.parent / 'optimization_results'
self.output_dir.mkdir(exist_ok=True)
# Initialize plugin/hook system
self.hook_manager = HookManager()
plugins_dir = Path(__file__).parent / 'plugins'
if plugins_dir.exists():
self.hook_manager.load_plugins_from_directory(plugins_dir)
summary = self.hook_manager.get_summary()
if summary['total_hooks'] > 0:
print(f"Loaded {summary['enabled_hooks']}/{summary['total_hooks']} plugins")
def _load_config(self) -> Dict[str, Any]:
"""Load and validate optimization configuration."""
with open(self.config_path, 'r') as f:
@@ -311,6 +322,16 @@ class OptimizationRunner:
int(dv['bounds'][1])
)
# Execute pre_solve hooks
pre_solve_context = {
'trial_number': trial.number,
'design_variables': design_vars,
'sim_file': self.config.get('sim_file', ''),
'working_dir': str(Path.cwd()),
'config': self.config
}
self.hook_manager.execute_hooks('pre_solve', pre_solve_context, fail_fast=False)
# 2. Update NX model with new parameters
try:
self.model_updater(design_vars)
@@ -318,6 +339,15 @@ class OptimizationRunner:
print(f"Error updating model: {e}")
raise optuna.TrialPruned()
# Execute post_mesh hooks (after model update)
post_mesh_context = {
'trial_number': trial.number,
'design_variables': design_vars,
'sim_file': self.config.get('sim_file', ''),
'working_dir': str(Path.cwd())
}
self.hook_manager.execute_hooks('post_mesh', post_mesh_context, fail_fast=False)
# 3. Run simulation
try:
result_path = self.simulation_runner()
@@ -325,6 +355,15 @@ class OptimizationRunner:
print(f"Error running simulation: {e}")
raise optuna.TrialPruned()
# Execute post_solve hooks
post_solve_context = {
'trial_number': trial.number,
'design_variables': design_vars,
'result_path': str(result_path) if result_path else '',
'working_dir': str(Path.cwd())
}
self.hook_manager.execute_hooks('post_solve', post_solve_context, fail_fast=False)
# 4. Extract results with appropriate precision
extracted_results = {}
for obj in self.config['objectives']:
@@ -362,6 +401,16 @@ class OptimizationRunner:
print(f"Error extracting {const['name']}: {e}")
raise optuna.TrialPruned()
# Execute post_extraction hooks
post_extraction_context = {
'trial_number': trial.number,
'design_variables': design_vars,
'extracted_results': extracted_results,
'result_path': str(result_path) if result_path else '',
'working_dir': str(Path.cwd())
}
self.hook_manager.execute_hooks('post_extraction', post_extraction_context, fail_fast=False)
# 5. Evaluate constraints
for const in self.config.get('constraints', []):
value = extracted_results[const['name']]
@@ -389,6 +438,23 @@ class OptimizationRunner:
else: # maximize
total_objective -= weight * value
# Execute custom_objective hooks (can modify total_objective)
custom_objective_context = {
'trial_number': trial.number,
'design_variables': design_vars,
'extracted_results': extracted_results,
'total_objective': total_objective,
'working_dir': str(Path.cwd())
}
custom_results = self.hook_manager.execute_hooks('custom_objective', custom_objective_context, fail_fast=False)
# Allow hooks to override objective value
for result in custom_results:
if result and 'total_objective' in result:
total_objective = result['total_objective']
print(f"Custom objective hook modified total_objective to {total_objective:.6f}")
break # Use first hook that provides override
# 7. Store results in history
history_entry = {
'trial_number': trial.number,