""" 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(debug=False) 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', 'cpenta_stress', 'cbar_stress', 'cbeam_stress' ] max_stress_overall = 0.0 max_element_id = None max_element_type = None # Try to get stress from different pyNastran API formats for table_name in stress_tables: stress_table = None # Try format 1: Attribute name with dot (e.g., 'stress.chexa_stress') # This is used in newer pyNastran versions dotted_name = f'stress.{table_name}' if hasattr(op2, dotted_name): stress_table = getattr(op2, dotted_name) # Try format 2: Nested attribute op2.stress.chexa_stress elif hasattr(op2, 'stress') and hasattr(op2.stress, table_name): stress_table = getattr(op2.stress, table_name) # Try format 3: Direct attribute op2.chexa_stress (older pyNastran) elif 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': # For solid elements (CHEXA, CTETRA, CPENTA): von Mises is at index 9 # For shell elements (CQUAD4, CTRIA3): von Mises is last column (-1) if table_name in ['chexa_stress', 'ctetra_stress', 'cpenta_stress']: # Solid elements: data shape is [itime, nnodes, 10] # Index 9 is von_mises [oxx, oyy, ozz, txy, tyz, txz, o1, o2, o3, von_mises] stresses = stress_data.data[0, :, 9] else: # Shell elements: von Mises is last column stresses = stress_data.data[0, :, -1] else: # Max principal stress if table_name in ['chexa_stress', 'ctetra_stress', 'cpenta_stress']: stresses = stress_data.data[0, :, 6] # o1 (max principal) else: 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', '') # CRITICAL: NX Nastran outputs stress in kPa (mN/mm²), convert to MPa # 1 kPa = 0.001 MPa max_stress_overall_mpa = max_stress_overall / 1000.0 return { 'max_stress': float(max_stress_overall_mpa), 'stress_type': stress_type, 'element_id': int(max_element_id) if max_element_id else None, 'element_type': max_element_type, 'units': '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 ") 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']}")