""" Extract Principal Stresses from Structural Analysis ==================================================== Extracts principal stresses (sigma1, sigma2, sigma3) from OP2 files. Useful for failure criteria beyond von Mises (e.g., Tresca, max principal). Pattern: solid_stress Element Types: CTETRA, CHEXA, CQUAD4, CTRIA3 Result Type: principal stress API: model.op2_results.stress.ctetra_stress[subcase] Phase 2 Task 2.3 - NX Open Automation Roadmap """ from pathlib import Path from typing import Dict, Any, Optional, List, Literal import numpy as np from pyNastran.op2.op2 import OP2 # Column indices for principal stresses by element type # Based on pyNastran stress data layout STRESS_COLUMNS = { # Solid elements (CTETRA, CHEXA, CPENTA): 10 columns # [o1, o2, o3, ovm, omax_shear, ...] format varies 'ctetra': {'o1': 0, 'o2': 1, 'o3': 2, 'von_mises': 9, 'max_shear': 8}, 'chexa': {'o1': 0, 'o2': 1, 'o3': 2, 'von_mises': 9, 'max_shear': 8}, 'cpenta': {'o1': 0, 'o2': 1, 'o3': 2, 'von_mises': 9, 'max_shear': 8}, # Shell elements (CQUAD4, CTRIA3): 8 columns per surface # [fiber_dist, oxx, oyy, txy, angle, o1, o2, von_mises] 'cquad4': {'o1': 5, 'o2': 6, 'von_mises': 7}, 'ctria3': {'o1': 5, 'o2': 6, 'von_mises': 7}, } def extract_principal_stress( op2_file: Path, subcase: int = 1, element_type: str = 'ctetra', principal: Literal['max', 'mid', 'min', 'all'] = 'max' ) -> Dict[str, Any]: """ Extract principal stresses from solid or shell elements. Principal stresses are the eigenvalues of the stress tensor, ordered as: sigma1 >= sigma2 >= sigma3 (o1 >= o2 >= o3) Args: op2_file: Path to .op2 file subcase: Subcase ID (default 1) element_type: Element type ('ctetra', 'chexa', 'cquad4', 'ctria3') principal: Which principal stress to return: - 'max': Maximum principal (sigma1, tension positive) - 'mid': Middle principal (sigma2) - 'min': Minimum principal (sigma3, compression negative) - 'all': Return all three principals Returns: dict: { 'max_principal': Maximum principal stress value, 'min_principal': Minimum principal stress value, 'mid_principal': Middle principal stress (if applicable), 'max_element': Element ID with maximum principal, 'min_element': Element ID with minimum principal, 'von_mises_max': Max von Mises for comparison, 'element_type': Element type used, 'subcase': Subcase ID, 'units': 'MPa (model units)' } Example: >>> result = extract_principal_stress('model.op2', element_type='ctetra') >>> print(f"Max tension: {result['max_principal']:.2f} MPa") >>> print(f"Max compression: {result['min_principal']:.2f} MPa") """ model = OP2() model.read_op2(str(op2_file)) # Map element type to stress attribute stress_attr_map = { 'ctetra': 'ctetra_stress', 'chexa': 'chexa_stress', 'cpenta': 'cpenta_stress', 'cquad4': 'cquad4_stress', 'ctria3': 'ctria3_stress' } element_type_lower = element_type.lower() stress_attr = stress_attr_map.get(element_type_lower) if not stress_attr: raise ValueError(f"Unknown element type: {element_type}. " f"Supported: {list(stress_attr_map.keys())}") # Access stress through op2_results container stress_dict = None if hasattr(model, 'op2_results') and hasattr(model.op2_results, 'stress'): stress_container = model.op2_results.stress if hasattr(stress_container, stress_attr): stress_dict = getattr(stress_container, stress_attr) if stress_dict is None or not stress_dict: available = [a for a in dir(model) if 'stress' in a.lower()] raise ValueError(f"No {element_type} stress results in OP2. " f"Available: {available}") # Get subcase available_subcases = list(stress_dict.keys()) if subcase in available_subcases: actual_subcase = subcase else: actual_subcase = available_subcases[0] stress = stress_dict[actual_subcase] # Get column indices for this element type cols = STRESS_COLUMNS.get(element_type_lower) if cols is None: # Fallback: assume standard layout cols = {'o1': 0, 'o2': 1, 'o3': 2 if element_type_lower in ['ctetra', 'chexa', 'cpenta'] else None} itime = 0 # First time step (static analysis) # Extract principal stresses o1 = stress.data[itime, :, cols['o1']] # Max principal o2 = stress.data[itime, :, cols['o2']] # Mid principal # Solid elements have 3 principals, shells have 2 if 'o3' in cols and cols['o3'] is not None: o3 = stress.data[itime, :, cols['o3']] # Min principal else: o3 = None # Get element IDs element_ids = np.array([eid for (eid, node) in stress.element_node]) # Find extremes max_o1_idx = np.argmax(o1) min_o1_idx = np.argmin(o1) result = { 'max_principal': float(np.max(o1)), 'max_principal_element': int(element_ids[max_o1_idx]), 'min_principal_o1': float(np.min(o1)), 'min_principal_o1_element': int(element_ids[min_o1_idx]), 'mean_principal': float(np.mean(o1)), 'element_type': element_type, 'subcase': actual_subcase, 'units': 'MPa (model units)', 'num_elements': len(element_ids), } # Add mid principal stats result['max_mid_principal'] = float(np.max(o2)) result['min_mid_principal'] = float(np.min(o2)) # Add minimum principal (sigma3) for solid elements if o3 is not None: min_o3_idx = np.argmin(o3) # Most negative = max compression max_o3_idx = np.argmax(o3) result['max_min_principal'] = float(np.max(o3)) result['min_min_principal'] = float(np.min(o3)) # Most compressive result['min_principal_element'] = int(element_ids[min_o3_idx]) result['has_three_principals'] = True else: result['has_three_principals'] = False # Add von Mises for comparison if 'von_mises' in cols: vm = stress.data[itime, :, cols['von_mises']] result['von_mises_max'] = float(np.max(vm)) result['von_mises_mean'] = float(np.mean(vm)) return result def extract_max_principal_stress( op2_file: Path, subcase: int = 1, element_type: str = 'ctetra' ) -> float: """ Convenience function to extract maximum principal stress. Args: op2_file: Path to .op2 file subcase: Subcase ID element_type: Element type Returns: Maximum principal stress value (tension positive) """ result = extract_principal_stress(op2_file, subcase, element_type) return result['max_principal'] def extract_min_principal_stress( op2_file: Path, subcase: int = 1, element_type: str = 'ctetra' ) -> float: """ Convenience function to extract minimum principal stress. For solid elements, returns sigma3 (most compressive). For shell elements, returns sigma2. Args: op2_file: Path to .op2 file subcase: Subcase ID element_type: Element type Returns: Minimum principal stress value (compression negative) """ result = extract_principal_stress(op2_file, subcase, element_type) if result['has_three_principals']: return result['min_min_principal'] else: return result['min_mid_principal'] if __name__ == '__main__': import sys if len(sys.argv) > 1: op2_file = Path(sys.argv[1]) element_type = sys.argv[2] if len(sys.argv) > 2 else 'ctetra' result = extract_principal_stress(op2_file, element_type=element_type) print(f"\nPrincipal Stress Results ({element_type}):") print(f" Max Principal (σ1): {result['max_principal']:.2f} MPa") print(f" Element: {result['max_principal_element']}") if result.get('has_three_principals'): print(f" Min Principal (σ3): {result['min_min_principal']:.2f} MPa") print(f" Element: {result['min_principal_element']}") if 'von_mises_max' in result: print(f" Von Mises Max: {result['von_mises_max']:.2f} MPa") print(f" Elements analyzed: {result['num_elements']}") else: print(f"Usage: python {sys.argv[0]} [element_type]")