feat: Add MLP surrogate with Turbo Mode for 100x faster optimization

Neural Acceleration (MLP Surrogate):
- Add run_nn_optimization.py with hybrid FEA/NN workflow
- MLP architecture: 4-layer (64->128->128->64) with BatchNorm/Dropout
- Three workflow modes:
  - --all: Sequential export->train->optimize->validate
  - --hybrid-loop: Iterative Train->NN->Validate->Retrain cycle
  - --turbo: Aggressive single-best validation (RECOMMENDED)
- Turbo mode: 5000 NN trials + 50 FEA validations in ~12 minutes
- Separate nn_study.db to avoid overloading dashboard

Performance Results (bracket_pareto_3obj study):
- NN prediction errors: mass 1-5%, stress 1-4%, stiffness 5-15%
- Found minimum mass designs at boundary (angle~30deg, thick~30mm)
- 100x speedup vs pure FEA exploration

Protocol Operating System:
- Add .claude/skills/ with Bootstrap, Cheatsheet, Context Loader
- Add docs/protocols/ with operations (OP_01-06) and system (SYS_10-14)
- Update SYS_14_NEURAL_ACCELERATION.md with MLP Turbo Mode docs

NX Automation:
- Add optimization_engine/hooks/ for NX CAD/CAE automation
- Add study_wizard.py for guided study creation
- Fix FEM mesh update: load idealized part before UpdateFemodel()

New Study:
- bracket_pareto_3obj: 3-objective Pareto (mass, stress, stiffness)
- 167 FEA trials + 5000 NN trials completed
- Demonstrates full hybrid workflow

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Antoine
2025-12-06 20:01:59 -05:00
parent 0cb2808c44
commit 602560c46a
70 changed files with 31018 additions and 289 deletions

View File

@@ -0,0 +1,186 @@
"""
Extract {Physics Name} from FEA results.
This is a template for creating new physics extractors.
Copy this file to optimization_engine/extractors/extract_{physics}.py
and customize for your specific physics extraction.
Author: {Your Name}
Created: {Date}
Version: 1.0
"""
from pathlib import Path
from typing import Dict, Any, Optional, Union
from pyNastran.op2.op2 import OP2
def extract_{physics}(
op2_file: Union[str, Path],
subcase: int = 1,
# Add other parameters specific to your physics
) -> Dict[str, Any]:
"""
Extract {physics description} from OP2 file.
Args:
op2_file: Path to the OP2 results file
subcase: Subcase number to extract (default: 1)
# Document other parameters
Returns:
Dictionary containing:
- '{main_result}': The primary result value ({unit})
- '{secondary_result}': Secondary result info
- 'subcase': The subcase extracted
- 'unit': Unit of the result
Raises:
FileNotFoundError: If OP2 file doesn't exist
KeyError: If subcase not found in results
ValueError: If result data is invalid
Example:
>>> result = extract_{physics}('model.op2', subcase=1)
>>> print(result['{main_result}'])
123.45
>>> print(result['unit'])
'{unit}'
"""
# Convert to Path for consistency
op2_file = Path(op2_file)
# Validate file exists
if not op2_file.exists():
raise FileNotFoundError(f"OP2 file not found: {op2_file}")
# Read OP2 file
op2 = OP2()
op2.read_op2(str(op2_file))
# =========================================
# CUSTOMIZE: Your extraction logic here
# =========================================
# Example: Access displacement data
# if subcase not in op2.displacements:
# raise KeyError(f"Subcase {subcase} not found in displacement results")
# data = op2.displacements[subcase]
# Example: Access stress data
# if subcase not in op2.cquad4_stress:
# raise KeyError(f"Subcase {subcase} not found in stress results")
# stress_data = op2.cquad4_stress[subcase]
# Example: Process data
# values = data.data # numpy array
# max_value = values.max()
# max_index = values.argmax()
# =========================================
# Replace with your actual computation
# =========================================
main_result = 0.0 # TODO: Compute actual value
secondary_result = 0 # TODO: Compute actual value
return {
'{main_result}': main_result,
'{secondary_result}': secondary_result,
'subcase': subcase,
'unit': '{unit}',
}
# Optional: Class-based extractor for complex cases
class {Physics}Extractor:
"""
Class-based extractor for {physics} with state management.
Use this pattern when:
- Extraction requires multiple steps
- You need to cache the OP2 data
- Configuration is complex
Example:
>>> extractor = {Physics}Extractor('model.op2', config={'option': value})
>>> result = extractor.extract(subcase=1)
>>> print(result)
"""
def __init__(
self,
op2_file: Union[str, Path],
bdf_file: Optional[Union[str, Path]] = None,
**config
):
"""
Initialize the extractor.
Args:
op2_file: Path to OP2 results file
bdf_file: Optional path to BDF mesh file (for node coordinates)
**config: Additional configuration options
"""
self.op2_file = Path(op2_file)
self.bdf_file = Path(bdf_file) if bdf_file else None
self.config = config
self._op2 = None # Lazy-loaded
def _load_op2(self) -> OP2:
"""Lazy load OP2 file (caches result)."""
if self._op2 is None:
self._op2 = OP2()
self._op2.read_op2(str(self.op2_file))
return self._op2
def extract(self, subcase: int = 1) -> Dict[str, Any]:
"""
Extract results for given subcase.
Args:
subcase: Subcase number
Returns:
Dictionary with extraction results
"""
op2 = self._load_op2()
# TODO: Implement your extraction logic
# Use self.config for configuration options
return {
'{main_result}': 0.0,
'subcase': subcase,
}
def extract_all_subcases(self) -> Dict[int, Dict[str, Any]]:
"""
Extract results for all available subcases.
Returns:
Dictionary mapping subcase number to results
"""
op2 = self._load_op2()
# TODO: Find available subcases
# available_subcases = list(op2.displacements.keys())
results = {}
# for sc in available_subcases:
# results[sc] = self.extract(subcase=sc)
return results
# =========================================
# After creating your extractor:
# 1. Add to optimization_engine/extractors/__init__.py:
# from .extract_{physics} import extract_{physics}
# __all__ = [..., 'extract_{physics}']
#
# 2. Update docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md
# - Add to Quick Reference table
# - Add detailed section with example
#
# 3. Create test file: tests/test_extract_{physics}.py
# =========================================

View File

@@ -0,0 +1,213 @@
"""
{Hook Name} - Lifecycle Hook Plugin
This is a template for creating new lifecycle hooks.
Copy this file to optimization_engine/plugins/{hook_point}/{hook_name}.py
Available hook points:
- pre_mesh: Before meshing
- post_mesh: After meshing
- pre_solve: Before solver execution
- post_solve: After solver completion
- post_extraction: After result extraction
- post_calculation: After objective calculation
- custom_objective: Custom objective functions
Author: {Your Name}
Created: {Date}
Version: 1.0
Hook Point: {hook_point}
"""
from typing import Dict, Any, Optional
from pathlib import Path
import json
from datetime import datetime
def {hook_name}_hook(context: Dict[str, Any]) -> Dict[str, Any]:
"""
{Description of what this hook does}.
This hook runs at the {hook_point} stage of the optimization trial.
Args:
context: Dictionary containing trial context:
- trial_number (int): Current trial number
- design_params (dict): Current design parameter values
- config (dict): Optimization configuration
- working_dir (Path): Study working directory
For post_solve and later:
- op2_file (Path): Path to OP2 results file
- solve_success (bool): Whether solve succeeded
- solve_time (float): Solve duration in seconds
For post_extraction and later:
- results (dict): Extracted results so far
For post_calculation:
- objectives (dict): Computed objective values
- constraints (dict): Constraint values
Returns:
Dictionary with computed values or modifications.
These values are added to the trial context.
Return empty dict {} if no modifications needed.
Raises:
Exception: Any exception will be logged but won't stop the trial
unless you want it to (raise optuna.TrialPruned instead)
Example:
>>> context = {'trial_number': 1, 'design_params': {'x': 5.0}}
>>> result = {hook_name}_hook(context)
>>> print(result)
{{'{computed_key}': 123.45}}
"""
# =========================================
# Access context values
# =========================================
trial_num = context.get('trial_number', 0)
design_params = context.get('design_params', {})
config = context.get('config', {})
working_dir = context.get('working_dir', Path('.'))
# For post_solve hooks and later:
# op2_file = context.get('op2_file')
# solve_success = context.get('solve_success', False)
# For post_extraction hooks and later:
# results = context.get('results', {})
# For post_calculation hooks:
# objectives = context.get('objectives', {})
# constraints = context.get('constraints', {})
# =========================================
# Your hook logic here
# =========================================
# Example: Log trial start (pre_solve hook)
# print(f"[Hook] Trial {trial_num} starting with params: {design_params}")
# Example: Compute derived quantity (post_extraction hook)
# max_stress = results.get('max_von_mises', 0)
# yield_strength = config.get('material', {}).get('yield_strength', 250)
# safety_factor = yield_strength / max(max_stress, 1e-6)
# Example: Write log file (post_calculation hook)
# log_entry = {
# 'trial': trial_num,
# 'timestamp': datetime.now().isoformat(),
# 'objectives': context.get('objectives', {}),
# }
# with open(working_dir / 'trial_log.jsonl', 'a') as f:
# f.write(json.dumps(log_entry) + '\n')
# =========================================
# Return computed values
# =========================================
# Values returned here are added to the context
# and can be accessed by later hooks or the optimizer
return {
# '{computed_key}': computed_value,
}
def register_hooks(hook_manager) -> None:
"""
Register this hook with the hook manager.
This function is called automatically when plugins are discovered.
It must be named exactly 'register_hooks' and take one argument.
Args:
hook_manager: The HookManager instance from optimization_engine
"""
hook_manager.register_hook(
hook_point='{hook_point}', # pre_mesh, post_mesh, pre_solve, etc.
function={hook_name}_hook,
name='{hook_name}_hook',
description='{Brief description of what this hook does}',
priority=100, # Lower number = runs earlier (1-200 typical range)
enabled=True # Set to False to disable by default
)
# =========================================
# Optional: Helper functions
# =========================================
def _helper_function(data: Any) -> Any:
"""
Private helper function for the hook.
Keep hook logic clean by extracting complex operations
into helper functions.
"""
pass
# =========================================
# After creating your hook:
#
# 1. Place in correct directory:
# optimization_engine/plugins/{hook_point}/{hook_name}.py
#
# 2. Hook is auto-discovered - no __init__.py changes needed
#
# 3. Test the hook:
# python -c "
# from optimization_engine.plugins.hook_manager import HookManager
# hm = HookManager()
# hm.discover_plugins()
# print(hm.list_hooks())
# "
#
# 4. Update documentation if significant:
# - Add to EXT_02_CREATE_HOOK.md examples section
# =========================================
# =========================================
# Example hooks for reference
# =========================================
def example_logger_hook(context: Dict[str, Any]) -> Dict[str, Any]:
"""Example: Simple trial logger for pre_solve."""
trial = context.get('trial_number', 0)
params = context.get('design_params', {})
print(f"[LOG] Trial {trial} starting: {params}")
return {}
def example_safety_factor_hook(context: Dict[str, Any]) -> Dict[str, Any]:
"""Example: Safety factor calculator for post_extraction."""
results = context.get('results', {})
max_stress = results.get('max_von_mises', 0)
if max_stress > 0:
safety_factor = 250.0 / max_stress # Assuming 250 MPa yield
else:
safety_factor = float('inf')
return {'safety_factor': safety_factor}
def example_validator_hook(context: Dict[str, Any]) -> Dict[str, Any]:
"""Example: Result validator for post_solve."""
import optuna
solve_success = context.get('solve_success', False)
op2_file = context.get('op2_file')
if not solve_success:
raise optuna.TrialPruned("Solve failed")
if op2_file and not Path(op2_file).exists():
raise optuna.TrialPruned("OP2 file not generated")
return {'validation_passed': True}

View File

@@ -0,0 +1,112 @@
# {LAYER}_{NUMBER}_{NAME}
<!--
PROTOCOL: {Full Protocol Name}
LAYER: {Operations|System|Extensions}
VERSION: 1.0
STATUS: Active
LAST_UPDATED: {YYYY-MM-DD}
PRIVILEGE: {user|power_user|admin}
LOAD_WITH: [{dependency_protocols}]
-->
## Overview
{1-3 sentence description of what this protocol does and why it exists.}
---
## When to Use
| Trigger | Action |
|---------|--------|
| {keyword or user intent} | Follow this protocol |
| {condition} | Follow this protocol |
---
## Quick Reference
{Key information in table format for fast lookup}
| Parameter | Default | Description |
|-----------|---------|-------------|
| {param} | {value} | {description} |
---
## Detailed Specification
### Section 1: {Topic}
{Detailed content}
```python
# Code example if applicable
```
### Section 2: {Topic}
{Detailed content}
---
## Configuration
{If applicable, show configuration examples}
```json
{
"setting": "value"
}
```
---
## Examples
### Example 1: {Scenario Name}
{Complete working example with context}
```python
# Full working code example
```
### Example 2: {Scenario Name}
{Another example showing different use case}
---
## Troubleshooting
| Symptom | Cause | Solution |
|---------|-------|----------|
| {error message or symptom} | {root cause} | {how to fix} |
| {symptom} | {cause} | {solution} |
---
## Cross-References
- **Depends On**: [{protocol_name}]({relative_path})
- **Used By**: [{protocol_name}]({relative_path})
- **See Also**: [{related_doc}]({path})
---
## Implementation Files
{If applicable, list the code files that implement this protocol}
- `path/to/file.py` - {description}
- `path/to/other.py` - {description}
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | {YYYY-MM-DD} | Initial release |