Enhanced expression extraction to find ALL named expressions in .prt files, not just specific format. Added pyNastran-based result extraction example. Expression Extraction Improvements: - Updated regex to handle all NX expression format variations: * #(Type [units]) name: value; * (Type [units]) name: value; * *(Type [units]) name: value; * ((Type [units]) name: value; - Added Root:expression_name: pattern detection - Finds expressions even when value is not immediately available - Deduplication to avoid duplicates - Filters out NX internal names Test Results with Bracket.prt: - Previously: 1 expression (tip_thickness only) - Now: 5 expressions found: * support_angle = 30.0 degrees * tip_thickness = 20.0 mm * p3 = 10.0 mm * support_blend_radius = 10.0 mm * p11 (reference found, value unknown) OP2 Result Extraction (pyNastran): - Created example extractor: op2_extractor_example.py - Functions for common optimization metrics: * extract_max_displacement() - max displacement magnitude on any node * extract_max_stress() - von Mises or max principal stress * extract_mass() - total mass and center of gravity - Handles multiple element types (CQUAD4, CTRIA3, CTETRA, etc.) - Returns structured JSON for optimization engine integration - Command-line tool for testing with real OP2 files Usage: python optimization_engine/result_extractors/op2_extractor_example.py <file.op2> Integration Ready: - pyNastran already in requirements.txt - Result extractor pattern established - Can be used as template for custom metrics Next Steps: - Integrate result extractors into MCP tool framework - Add safety factor calculations - Support for thermal, modal results
237 lines
6.8 KiB
Python
237 lines
6.8 KiB
Python
"""
|
|
Example: Result Extraction from OP2 files using pyNastran
|
|
|
|
This shows how to extract optimization metrics from Nastran OP2 files.
|
|
Common metrics:
|
|
- Max displacement (for stiffness constraints)
|
|
- Max von Mises stress (for strength constraints)
|
|
- Mass (for minimization objectives)
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, Any
|
|
import numpy as np
|
|
|
|
|
|
def extract_max_displacement(op2_path: Path) -> Dict[str, Any]:
|
|
"""
|
|
Extract maximum displacement magnitude from OP2 file.
|
|
|
|
Args:
|
|
op2_path: Path to .op2 file
|
|
|
|
Returns:
|
|
Dictionary with max displacement, node ID, and components
|
|
"""
|
|
from pyNastran.op2.op2 import OP2
|
|
|
|
op2 = OP2()
|
|
op2.read_op2(str(op2_path))
|
|
|
|
# Get first subcase (usually the only one in static analysis)
|
|
subcase_id = list(op2.displacements.keys())[0]
|
|
displacements = op2.displacements[subcase_id]
|
|
|
|
# Extract node IDs and displacement data
|
|
node_ids = displacements.node_gridtype[:, 0].astype(int)
|
|
disp_data = displacements.data[0] # First (and usually only) timestep
|
|
|
|
# Calculate magnitude: sqrt(dx^2 + dy^2 + dz^2)
|
|
dx = disp_data[:, 0]
|
|
dy = disp_data[:, 1]
|
|
dz = disp_data[:, 2]
|
|
magnitudes = np.sqrt(dx**2 + dy**2 + dz**2)
|
|
|
|
# Find max
|
|
max_idx = np.argmax(magnitudes)
|
|
max_displacement = magnitudes[max_idx]
|
|
max_node_id = node_ids[max_idx]
|
|
|
|
return {
|
|
'max_displacement': float(max_displacement),
|
|
'max_node_id': int(max_node_id),
|
|
'dx': float(dx[max_idx]),
|
|
'dy': float(dy[max_idx]),
|
|
'dz': float(dz[max_idx]),
|
|
'units': 'mm', # NX typically uses mm
|
|
'subcase': subcase_id
|
|
}
|
|
|
|
|
|
def extract_max_stress(op2_path: Path, stress_type: str = 'von_mises') -> Dict[str, Any]:
|
|
"""
|
|
Extract maximum stress from OP2 file.
|
|
|
|
Args:
|
|
op2_path: Path to .op2 file
|
|
stress_type: 'von_mises' or 'max_principal'
|
|
|
|
Returns:
|
|
Dictionary with max stress, element ID, and location
|
|
"""
|
|
from pyNastran.op2.op2 import OP2
|
|
|
|
op2 = OP2()
|
|
op2.read_op2(str(op2_path))
|
|
|
|
# Stress can be in different tables depending on element type
|
|
# Common: cquad4_stress, ctria3_stress, ctetra_stress, etc.
|
|
stress_tables = [
|
|
'cquad4_stress',
|
|
'ctria3_stress',
|
|
'ctetra_stress',
|
|
'chexa_stress',
|
|
'cbar_stress',
|
|
'cbeam_stress'
|
|
]
|
|
|
|
max_stress_overall = 0.0
|
|
max_element_id = None
|
|
max_element_type = None
|
|
|
|
for table_name in stress_tables:
|
|
if hasattr(op2, table_name):
|
|
stress_table = getattr(op2, table_name)
|
|
if stress_table:
|
|
subcase_id = list(stress_table.keys())[0]
|
|
stress_data = stress_table[subcase_id]
|
|
|
|
# Extract von Mises stress
|
|
# Note: Structure varies by element type
|
|
element_ids = stress_data.element_node[:, 0].astype(int)
|
|
|
|
if stress_type == 'von_mises':
|
|
# von Mises is usually last column
|
|
stresses = stress_data.data[0, :, -1] # timestep 0, all elements, last column
|
|
else:
|
|
# Max principal stress (second-to-last column typically)
|
|
stresses = stress_data.data[0, :, -2]
|
|
|
|
max_stress_in_table = np.max(stresses)
|
|
if max_stress_in_table > max_stress_overall:
|
|
max_stress_overall = max_stress_in_table
|
|
max_idx = np.argmax(stresses)
|
|
max_element_id = element_ids[max_idx]
|
|
max_element_type = table_name.replace('_stress', '')
|
|
|
|
return {
|
|
'max_stress': float(max_stress_overall),
|
|
'stress_type': stress_type,
|
|
'element_id': int(max_element_id) if max_element_id else None,
|
|
'element_type': max_element_type,
|
|
'units': 'MPa', # NX typically uses MPa
|
|
}
|
|
|
|
|
|
def extract_mass(op2_path: Path) -> Dict[str, Any]:
|
|
"""
|
|
Extract total mass from OP2 file.
|
|
|
|
Args:
|
|
op2_path: Path to .op2 file
|
|
|
|
Returns:
|
|
Dictionary with mass and center of gravity
|
|
"""
|
|
from pyNastran.op2.op2 import OP2
|
|
|
|
op2 = OP2()
|
|
op2.read_op2(str(op2_path))
|
|
|
|
# Mass is in grid_point_weight table
|
|
if hasattr(op2, 'grid_point_weight') and op2.grid_point_weight:
|
|
mass_data = op2.grid_point_weight
|
|
|
|
# Total mass
|
|
total_mass = mass_data.mass.sum()
|
|
|
|
# Center of gravity
|
|
cg = mass_data.cg
|
|
|
|
return {
|
|
'total_mass': float(total_mass),
|
|
'cg_x': float(cg[0]),
|
|
'cg_y': float(cg[1]),
|
|
'cg_z': float(cg[2]),
|
|
'units': 'kg'
|
|
}
|
|
else:
|
|
# Fallback: Mass not directly available
|
|
return {
|
|
'total_mass': None,
|
|
'note': 'Mass data not found in OP2 file. Ensure PARAM,GRDPNT,0 is in Nastran deck'
|
|
}
|
|
|
|
|
|
# Combined extraction function for optimization
|
|
def extract_all_results(op2_path: Path) -> Dict[str, Any]:
|
|
"""
|
|
Extract all common optimization metrics from OP2 file.
|
|
|
|
Args:
|
|
op2_path: Path to .op2 file
|
|
|
|
Returns:
|
|
Dictionary with all results
|
|
"""
|
|
results = {
|
|
'op2_file': str(op2_path),
|
|
'status': 'success'
|
|
}
|
|
|
|
try:
|
|
results['displacement'] = extract_max_displacement(op2_path)
|
|
except Exception as e:
|
|
results['displacement'] = {'error': str(e)}
|
|
|
|
try:
|
|
results['stress'] = extract_max_stress(op2_path)
|
|
except Exception as e:
|
|
results['stress'] = {'error': str(e)}
|
|
|
|
try:
|
|
results['mass'] = extract_mass(op2_path)
|
|
except Exception as e:
|
|
results['mass'] = {'error': str(e)}
|
|
|
|
return results
|
|
|
|
|
|
# Example usage
|
|
if __name__ == "__main__":
|
|
import sys
|
|
import json
|
|
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python op2_extractor_example.py <path_to_op2_file>")
|
|
sys.exit(1)
|
|
|
|
op2_path = Path(sys.argv[1])
|
|
|
|
if not op2_path.exists():
|
|
print(f"Error: File not found: {op2_path}")
|
|
sys.exit(1)
|
|
|
|
print(f"Extracting results from: {op2_path}")
|
|
print("=" * 60)
|
|
|
|
results = extract_all_results(op2_path)
|
|
|
|
print("\nResults:")
|
|
print(json.dumps(results, indent=2))
|
|
|
|
# Summary
|
|
print("\n" + "=" * 60)
|
|
print("SUMMARY:")
|
|
if 'displacement' in results and 'max_displacement' in results['displacement']:
|
|
disp = results['displacement']
|
|
print(f" Max Displacement: {disp['max_displacement']:.6f} {disp['units']} at node {disp['max_node_id']}")
|
|
|
|
if 'stress' in results and 'max_stress' in results['stress']:
|
|
stress = results['stress']
|
|
print(f" Max {stress['stress_type']}: {stress['max_stress']:.2f} {stress['units']} in element {stress['element_id']}")
|
|
|
|
if 'mass' in results and 'total_mass' in results['mass'] and results['mass']['total_mass']:
|
|
mass = results['mass']
|
|
print(f" Total Mass: {mass['total_mass']:.6f} {mass['units']}")
|