Implement core optimization engine with: - OptimizationRunner class with Optuna integration - NXParameterUpdater for updating .prt file expressions - Result extractor wrappers for OP2 files - Complete end-to-end example workflow Features: - runner.py: Main optimization loop, multi-objective support, constraint handling - nx_updater.py: Binary .prt file parameter updates (tested successfully) - extractors.py: Wrappers for mass/stress/displacement extraction - run_optimization.py: Complete example showing full workflow NX Updater tested with bracket example: - Successfully found 4 expressions (support_angle, tip_thickness, p3, support_blend_radius) - Updated support_angle 30.0 -> 33.0 and verified Next steps: - Install pyNastran for OP2 extraction - Integrate NX solver execution - Replace dummy extractors with real OP2 readers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
208 lines
5.8 KiB
Python
208 lines
5.8 KiB
Python
"""
|
|
Result Extractors
|
|
|
|
Wrapper functions that integrate with the optimization runner.
|
|
These extract optimization metrics from NX Nastran result files.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, Any
|
|
import sys
|
|
|
|
# Add project root to path
|
|
project_root = Path(__file__).parent.parent.parent
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
from optimization_engine.result_extractors.op2_extractor_example import (
|
|
extract_max_displacement,
|
|
extract_max_stress,
|
|
extract_mass
|
|
)
|
|
|
|
|
|
def mass_extractor(result_path: Path) -> Dict[str, float]:
|
|
"""
|
|
Extract mass metrics for optimization.
|
|
|
|
Args:
|
|
result_path: Path to .op2 file or directory containing results
|
|
|
|
Returns:
|
|
Dict with 'total_mass' and other mass-related metrics
|
|
"""
|
|
# If result_path is a directory, find the .op2 file
|
|
if result_path.is_dir():
|
|
op2_files = list(result_path.glob("*.op2"))
|
|
if not op2_files:
|
|
raise FileNotFoundError(f"No .op2 files found in {result_path}")
|
|
op2_path = op2_files[0] # Use first .op2 file
|
|
else:
|
|
op2_path = result_path
|
|
|
|
if not op2_path.exists():
|
|
raise FileNotFoundError(f"Result file not found: {op2_path}")
|
|
|
|
result = extract_mass(op2_path)
|
|
|
|
# Ensure total_mass key exists
|
|
if 'total_mass' not in result or result['total_mass'] is None:
|
|
raise ValueError(f"Could not extract total_mass from {op2_path}")
|
|
|
|
return result
|
|
|
|
|
|
def stress_extractor(result_path: Path) -> Dict[str, float]:
|
|
"""
|
|
Extract stress metrics for optimization.
|
|
|
|
Args:
|
|
result_path: Path to .op2 file or directory containing results
|
|
|
|
Returns:
|
|
Dict with 'max_von_mises' and other stress metrics
|
|
"""
|
|
# If result_path is a directory, find the .op2 file
|
|
if result_path.is_dir():
|
|
op2_files = list(result_path.glob("*.op2"))
|
|
if not op2_files:
|
|
raise FileNotFoundError(f"No .op2 files found in {result_path}")
|
|
op2_path = op2_files[0]
|
|
else:
|
|
op2_path = result_path
|
|
|
|
if not op2_path.exists():
|
|
raise FileNotFoundError(f"Result file not found: {op2_path}")
|
|
|
|
result = extract_max_stress(op2_path, stress_type='von_mises')
|
|
|
|
# Ensure max_von_mises key exists
|
|
if 'max_stress' in result:
|
|
result['max_von_mises'] = result['max_stress']
|
|
|
|
if 'max_von_mises' not in result or result['max_von_mises'] is None:
|
|
raise ValueError(f"Could not extract max_von_mises from {op2_path}")
|
|
|
|
return result
|
|
|
|
|
|
def displacement_extractor(result_path: Path) -> Dict[str, float]:
|
|
"""
|
|
Extract displacement metrics for optimization.
|
|
|
|
Args:
|
|
result_path: Path to .op2 file or directory containing results
|
|
|
|
Returns:
|
|
Dict with 'max_displacement' and other displacement metrics
|
|
"""
|
|
# If result_path is a directory, find the .op2 file
|
|
if result_path.is_dir():
|
|
op2_files = list(result_path.glob("*.op2"))
|
|
if not op2_files:
|
|
raise FileNotFoundError(f"No .op2 files found in {result_path}")
|
|
op2_path = op2_files[0]
|
|
else:
|
|
op2_path = result_path
|
|
|
|
if not op2_path.exists():
|
|
raise FileNotFoundError(f"Result file not found: {op2_path}")
|
|
|
|
result = extract_max_displacement(op2_path)
|
|
|
|
# Ensure max_displacement key exists
|
|
if 'max_displacement' not in result or result['max_displacement'] is None:
|
|
raise ValueError(f"Could not extract max_displacement from {op2_path}")
|
|
|
|
return result
|
|
|
|
|
|
def volume_extractor(result_path: Path) -> Dict[str, float]:
|
|
"""
|
|
Extract volume metrics for optimization.
|
|
|
|
Note: Volume is often not directly in OP2 files.
|
|
This is a placeholder that could be extended to:
|
|
- Calculate from mass and density
|
|
- Extract from .f06 file
|
|
- Query from NX model directly
|
|
|
|
Args:
|
|
result_path: Path to result files
|
|
|
|
Returns:
|
|
Dict with 'total_volume'
|
|
"""
|
|
# For now, estimate from mass (would need material density)
|
|
# This is a placeholder implementation
|
|
mass_result = mass_extractor(result_path)
|
|
|
|
# Assuming steel density ~7850 kg/m^3 = 7.85e-6 kg/mm^3
|
|
# volume (mm^3) = mass (kg) / density (kg/mm^3)
|
|
assumed_density = 7.85e-6 # kg/mm^3
|
|
|
|
if mass_result['total_mass']:
|
|
total_volume = mass_result['total_mass'] / assumed_density
|
|
else:
|
|
total_volume = None
|
|
|
|
return {
|
|
'total_volume': total_volume,
|
|
'note': 'Volume estimated from mass using assumed density'
|
|
}
|
|
|
|
|
|
# Registry of all available extractors
|
|
EXTRACTOR_REGISTRY = {
|
|
'mass_extractor': mass_extractor,
|
|
'stress_extractor': stress_extractor,
|
|
'displacement_extractor': displacement_extractor,
|
|
'volume_extractor': volume_extractor
|
|
}
|
|
|
|
|
|
def get_extractor(extractor_name: str):
|
|
"""
|
|
Get an extractor function by name.
|
|
|
|
Args:
|
|
extractor_name: Name of the extractor
|
|
|
|
Returns:
|
|
Extractor function
|
|
|
|
Raises:
|
|
ValueError: If extractor not found
|
|
"""
|
|
if extractor_name not in EXTRACTOR_REGISTRY:
|
|
available = ', '.join(EXTRACTOR_REGISTRY.keys())
|
|
raise ValueError(f"Unknown extractor: {extractor_name}. Available: {available}")
|
|
|
|
return EXTRACTOR_REGISTRY[extractor_name]
|
|
|
|
|
|
# Example usage
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python extractors.py <path_to_op2_file>")
|
|
print("\nAvailable extractors:")
|
|
for name in EXTRACTOR_REGISTRY.keys():
|
|
print(f" - {name}")
|
|
sys.exit(1)
|
|
|
|
result_path = Path(sys.argv[1])
|
|
|
|
print("="*60)
|
|
print("TESTING ALL EXTRACTORS")
|
|
print("="*60)
|
|
|
|
for extractor_name, extractor_func in EXTRACTOR_REGISTRY.items():
|
|
print(f"\n{extractor_name}:")
|
|
try:
|
|
result = extractor_func(result_path)
|
|
for key, value in result.items():
|
|
print(f" {key}: {value}")
|
|
except Exception as e:
|
|
print(f" Error: {e}")
|