feat: Add AtomizerField training data export and intelligent model discovery
Major additions: - Training data export system for AtomizerField neural network training - Bracket stiffness optimization study with 50+ training samples - Intelligent NX model discovery (auto-detect solutions, expressions, mesh) - Result extractors module for displacement, stress, frequency, mass - User-generated NX journals for advanced workflows - Archive structure for legacy scripts and test outputs - Protocol documentation and dashboard launcher 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
242
optimization_engine/extractors/op2_extractor.py
Normal file
242
optimization_engine/extractors/op2_extractor.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
Generic OP2 Extractor
|
||||
====================
|
||||
|
||||
Reusable extractor for NX Nastran OP2 files using pyNastran.
|
||||
Extracts mass properties, forces, displacements, stresses, etc.
|
||||
|
||||
Usage:
|
||||
extractor = OP2Extractor(op2_file="model.op2")
|
||||
mass = extractor.extract_mass()
|
||||
forces = extractor.extract_grid_point_forces()
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from pyNastran.op2.op2 import read_op2
|
||||
except ImportError:
|
||||
raise ImportError("pyNastran is required. Install with: pip install pyNastran")
|
||||
|
||||
|
||||
class OP2Extractor:
|
||||
"""
|
||||
Generic extractor for Nastran OP2 files.
|
||||
|
||||
Supports:
|
||||
- Mass properties
|
||||
- Grid point forces
|
||||
- Displacements
|
||||
- Stresses
|
||||
- Strains
|
||||
- Element forces
|
||||
"""
|
||||
|
||||
def __init__(self, op2_file: str):
|
||||
"""
|
||||
Args:
|
||||
op2_file: Path to .op2 file
|
||||
"""
|
||||
self.op2_file = Path(op2_file)
|
||||
self._op2_model = None
|
||||
|
||||
def _load_op2(self):
|
||||
"""Lazy load OP2 file"""
|
||||
if self._op2_model is None:
|
||||
if not self.op2_file.exists():
|
||||
raise FileNotFoundError(f"OP2 file not found: {self.op2_file}")
|
||||
self._op2_model = read_op2(str(self.op2_file), debug=False)
|
||||
return self._op2_model
|
||||
|
||||
def extract_mass(self, subcase_id: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract mass properties from OP2.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'mass_kg': total mass in kg,
|
||||
'mass_g': total mass in grams,
|
||||
'cg': [x, y, z] center of gravity,
|
||||
'inertia': 3x3 inertia matrix
|
||||
}
|
||||
"""
|
||||
op2 = self._load_op2()
|
||||
|
||||
# Get grid point weight (mass properties)
|
||||
if not hasattr(op2, 'grid_point_weight') or not op2.grid_point_weight:
|
||||
raise ValueError("No mass properties found in OP2 file")
|
||||
|
||||
gpw = op2.grid_point_weight
|
||||
|
||||
# Mass is typically in the first element of MO matrix (reference point mass)
|
||||
# OP2 stores mass in ton, mm, sec units typically
|
||||
mass_matrix = gpw.MO[0, 0] if hasattr(gpw, 'MO') else None
|
||||
|
||||
# Get reference point
|
||||
if hasattr(gpw, 'reference_point') and gpw.reference_point:
|
||||
ref_point = gpw.reference_point
|
||||
else:
|
||||
ref_point = 0
|
||||
|
||||
# Extract mass (convert based on units)
|
||||
# Nastran default: ton-mm-sec → need to convert to kg
|
||||
if mass_matrix is not None:
|
||||
mass_ton = mass_matrix
|
||||
mass_kg = mass_ton * 1000.0 # 1 ton = 1000 kg
|
||||
else:
|
||||
raise ValueError("Could not extract mass from OP2")
|
||||
|
||||
# Extract CG if available
|
||||
cg = [0.0, 0.0, 0.0]
|
||||
if hasattr(gpw, 'cg'):
|
||||
cg = gpw.cg.tolist() if hasattr(gpw.cg, 'tolist') else list(gpw.cg)
|
||||
|
||||
return {
|
||||
'mass_kg': mass_kg,
|
||||
'mass_g': mass_kg * 1000.0,
|
||||
'mass_ton': mass_ton,
|
||||
'cg': cg,
|
||||
'reference_point': ref_point,
|
||||
'units': 'ton-mm-sec (converted to kg)',
|
||||
}
|
||||
|
||||
def extract_grid_point_forces(
|
||||
self,
|
||||
subcase_id: Optional[int] = None,
|
||||
component: str = "total" # total, fx, fy, fz, mx, my, mz
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract grid point forces from OP2.
|
||||
|
||||
Args:
|
||||
subcase_id: Subcase ID (if None, uses first available)
|
||||
component: Force component to extract
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'force': resultant force value,
|
||||
'all_forces': list of forces at each grid point,
|
||||
'max_force': maximum force,
|
||||
'total_force': sum of all forces
|
||||
}
|
||||
"""
|
||||
op2 = self._load_op2()
|
||||
|
||||
if not hasattr(op2, 'grid_point_forces') or not op2.grid_point_forces:
|
||||
raise ValueError("No grid point forces found in OP2 file")
|
||||
|
||||
# Get first subcase if not specified
|
||||
if subcase_id is None:
|
||||
subcase_id = list(op2.grid_point_forces.keys())[0]
|
||||
|
||||
gpf = op2.grid_point_forces[subcase_id]
|
||||
|
||||
# Extract forces based on component
|
||||
# Grid point forces table typically has columns: fx, fy, fz, mx, my, mz
|
||||
if component == "total":
|
||||
# Calculate resultant force: sqrt(fx^2 + fy^2 + fz^2)
|
||||
forces = np.sqrt(gpf.data[:, 0]**2 + gpf.data[:, 1]**2 + gpf.data[:, 2]**2)
|
||||
elif component == "fx":
|
||||
forces = gpf.data[:, 0]
|
||||
elif component == "fy":
|
||||
forces = gpf.data[:, 1]
|
||||
elif component == "fz":
|
||||
forces = gpf.data[:, 2]
|
||||
else:
|
||||
raise ValueError(f"Unknown component: {component}")
|
||||
|
||||
return {
|
||||
'force': float(np.max(np.abs(forces))),
|
||||
'all_forces': forces.tolist(),
|
||||
'max_force': float(np.max(forces)),
|
||||
'min_force': float(np.min(forces)),
|
||||
'total_force': float(np.sum(forces)),
|
||||
'component': component,
|
||||
'subcase_id': subcase_id,
|
||||
}
|
||||
|
||||
def extract_applied_loads(self, subcase_id: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract applied loads from OP2 file.
|
||||
|
||||
This attempts to get load vector information if available.
|
||||
Note: Not all OP2 files contain this data.
|
||||
|
||||
Returns:
|
||||
dict: Load information
|
||||
"""
|
||||
op2 = self._load_op2()
|
||||
|
||||
# Try to get load vectors
|
||||
if hasattr(op2, 'load_vectors') and op2.load_vectors:
|
||||
if subcase_id is None:
|
||||
subcase_id = list(op2.load_vectors.keys())[0]
|
||||
|
||||
lv = op2.load_vectors[subcase_id]
|
||||
loads = lv.data
|
||||
|
||||
return {
|
||||
'total_load': float(np.sum(np.abs(loads))),
|
||||
'max_load': float(np.max(np.abs(loads))),
|
||||
'load_resultant': float(np.linalg.norm(loads)),
|
||||
'subcase_id': subcase_id,
|
||||
}
|
||||
else:
|
||||
# Fallback: use grid point forces as approximation
|
||||
return self.extract_grid_point_forces(subcase_id)
|
||||
|
||||
|
||||
def extract_mass_from_op2(op2_file: str) -> float:
|
||||
"""
|
||||
Convenience function to extract mass in kg.
|
||||
|
||||
Args:
|
||||
op2_file: Path to .op2 file
|
||||
|
||||
Returns:
|
||||
Mass in kilograms
|
||||
"""
|
||||
extractor = OP2Extractor(op2_file)
|
||||
result = extractor.extract_mass()
|
||||
return result['mass_kg']
|
||||
|
||||
|
||||
def extract_force_from_op2(
|
||||
op2_file: str,
|
||||
component: str = "fz"
|
||||
) -> float:
|
||||
"""
|
||||
Convenience function to extract force component.
|
||||
|
||||
Args:
|
||||
op2_file: Path to .op2 file
|
||||
component: Force component (fx, fy, fz, or total)
|
||||
|
||||
Returns:
|
||||
Force value
|
||||
"""
|
||||
extractor = OP2Extractor(op2_file)
|
||||
result = extractor.extract_grid_point_forces(component=component)
|
||||
return result['force']
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
op2_file = sys.argv[1]
|
||||
extractor = OP2Extractor(op2_file)
|
||||
|
||||
# Extract mass
|
||||
mass_result = extractor.extract_mass()
|
||||
print(f"Mass: {mass_result['mass_kg']:.6f} kg")
|
||||
print(f"CG: {mass_result['cg']}")
|
||||
|
||||
# Extract forces
|
||||
try:
|
||||
force_result = extractor.extract_grid_point_forces(component="fz")
|
||||
print(f"Max Fz: {force_result['force']:.2f} N")
|
||||
except ValueError as e:
|
||||
print(f"Forces not available: {e}")
|
||||
Reference in New Issue
Block a user