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>
243 lines
7.2 KiB
Python
243 lines
7.2 KiB
Python
"""
|
|
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}")
|