""" Extract Strain Energy from Structural Analysis =============================================== Extracts element strain energy and strain energy density from OP2 files. Strain energy is useful for topology optimization and structural efficiency metrics. Pattern: strain_energy Element Types: All structural elements Result Type: strain energy API: model.op2_results.strain_energy.*[subcase] Phase 2 Task 2.4 - NX Open Automation Roadmap """ from pathlib import Path from typing import Dict, Any, Optional, List import numpy as np from pyNastran.op2.op2 import OP2 def extract_strain_energy( op2_file: Path, subcase: int = 1, element_type: Optional[str] = None, top_n: int = 10 ) -> Dict[str, Any]: """ Extract strain energy from structural elements. Strain energy (U) is a measure of the work done to deform the structure: U = 0.5 * integral(sigma * epsilon) dV High strain energy density indicates highly stressed regions. Args: op2_file: Path to .op2 file subcase: Subcase ID (default 1) element_type: Filter by element type (e.g., 'ctetra', 'chexa', 'cquad4') If None, returns total from all elements top_n: Number of top elements to return by strain energy Returns: dict: { 'total_strain_energy': Total strain energy (all elements), 'mean_strain_energy': Mean strain energy per element, 'max_strain_energy': Maximum element strain energy, 'max_energy_element': Element ID with max strain energy, 'top_elements': List of (element_id, energy) tuples, 'energy_by_type': Dict of {element_type: total_energy}, 'num_elements': Total element count, 'subcase': Subcase ID, 'units': 'N-mm (model units)' } Example: >>> result = extract_strain_energy('model.op2') >>> print(f"Total strain energy: {result['total_strain_energy']:.2f} N-mm") >>> print(f"Highest energy element: {result['max_energy_element']}") """ model = OP2() model.read_op2(str(op2_file)) # Check for strain energy results # pyNastran stores strain energy in op2_results.strain_energy strain_energy_dict = None if hasattr(model, 'op2_results') and hasattr(model.op2_results, 'strain_energy'): se_container = model.op2_results.strain_energy # Strain energy is typically stored by element type # ctetra_strain_energy, chexa_strain_energy, etc. se_attrs = [a for a in dir(se_container) if 'strain_energy' in a.lower()] if not se_attrs: # Try direct access patterns if hasattr(se_container, 'strain_energy'): strain_energy_dict = se_container.strain_energy elif hasattr(model, 'strain_energy') and model.strain_energy: strain_energy_dict = model.strain_energy # Fallback: try legacy access pattern if strain_energy_dict is None and hasattr(model, 'ctetra_strain_energy'): strain_energy_dict = model.ctetra_strain_energy # Collect all strain energy data all_elements = [] all_energies = [] energy_by_type = {} # Search through all possible strain energy attributes se_attr_names = [ 'ctetra_strain_energy', 'chexa_strain_energy', 'cpenta_strain_energy', 'cquad4_strain_energy', 'ctria3_strain_energy', 'cbar_strain_energy', 'cbeam_strain_energy', 'crod_strain_energy', ] found_any = False for attr_name in se_attr_names: se_dict = None # Try op2_results container first if hasattr(model, 'op2_results') and hasattr(model.op2_results, 'strain_energy'): se_container = model.op2_results.strain_energy if hasattr(se_container, attr_name): se_dict = getattr(se_container, attr_name) # Fallback to direct model attribute if se_dict is None and hasattr(model, attr_name): se_dict = getattr(model, attr_name) if se_dict is None or not se_dict: continue # Extract element type from attribute name etype = attr_name.replace('_strain_energy', '') # Filter by element type if specified if element_type is not None and etype.lower() != element_type.lower(): continue # Get subcase available_subcases = list(se_dict.keys()) if not available_subcases: continue if subcase in available_subcases: actual_subcase = subcase else: actual_subcase = available_subcases[0] se_result = se_dict[actual_subcase] found_any = True # Extract data # Strain energy typically stored as: data[itime, ielement, icolumn] # Column 0 is usually total strain energy itime = 0 if hasattr(se_result, 'data'): energies = se_result.data[itime, :, 0] element_ids = se_result.element if hasattr(se_result, 'element') else np.arange(len(energies)) all_elements.extend(element_ids.tolist()) all_energies.extend(energies.tolist()) energy_by_type[etype] = float(np.sum(energies)) if not found_any or len(all_energies) == 0: # No strain energy found - this might not be requested in the analysis raise ValueError( "No strain energy results found in OP2 file. " "Ensure STRAIN=ALL or SEFINAL is in the Nastran case control." ) # Convert to numpy for analysis all_energies = np.array(all_energies) all_elements = np.array(all_elements) # Statistics total_energy = float(np.sum(all_energies)) mean_energy = float(np.mean(all_energies)) max_idx = np.argmax(all_energies) max_energy = float(all_energies[max_idx]) max_element = int(all_elements[max_idx]) # Top N elements by strain energy top_indices = np.argsort(all_energies)[-top_n:][::-1] top_elements = [ (int(all_elements[i]), float(all_energies[i])) for i in top_indices ] return { 'total_strain_energy': total_energy, 'mean_strain_energy': mean_energy, 'max_strain_energy': max_energy, 'max_energy_element': max_element, 'min_strain_energy': float(np.min(all_energies)), 'std_strain_energy': float(np.std(all_energies)), 'top_elements': top_elements, 'energy_by_type': energy_by_type, 'num_elements': len(all_elements), 'subcase': subcase, 'units': 'N-mm (model units)', } def extract_total_strain_energy( op2_file: Path, subcase: int = 1 ) -> float: """ Convenience function to extract total strain energy. Args: op2_file: Path to .op2 file subcase: Subcase ID Returns: Total strain energy (N-mm) """ result = extract_strain_energy(op2_file, subcase) return result['total_strain_energy'] def extract_strain_energy_density( op2_file: Path, subcase: int = 1, element_type: str = 'ctetra' ) -> Dict[str, Any]: """ Extract strain energy density (energy per volume). Strain energy density is useful for identifying critical regions and for material utilization optimization. Args: op2_file: Path to .op2 file subcase: Subcase ID element_type: Element type to analyze Returns: dict: { 'max_density': Maximum strain energy density, 'mean_density': Mean strain energy density, 'total_energy': Total strain energy, 'units': 'N/mm^2 (MPa equivalent)' } Note: This requires element volume data which may not always be available. Falls back to energy-only metrics if volume is unavailable. """ # For now, just return strain energy # Full implementation would require element volume from BDF or OP2 result = extract_strain_energy(op2_file, subcase, element_type) # Without volume data, we can't compute true density # Return energy metrics with a note return { 'max_strain_energy': result['max_strain_energy'], 'mean_strain_energy': result['mean_strain_energy'], 'total_strain_energy': result['total_strain_energy'], 'max_element': result['max_energy_element'], 'note': 'True density requires element volumes (not computed)', 'units': 'N-mm (energy), density requires volume' } if __name__ == '__main__': import sys if len(sys.argv) > 1: op2_file = Path(sys.argv[1]) try: result = extract_strain_energy(op2_file) print(f"\nStrain Energy Results:") print(f" Total: {result['total_strain_energy']:.4f} N-mm") print(f" Mean: {result['mean_strain_energy']:.4f} N-mm") print(f" Max: {result['max_strain_energy']:.4f} N-mm") print(f" Element: {result['max_energy_element']}") print(f"\n Energy by element type:") for etype, energy in result['energy_by_type'].items(): print(f" {etype}: {energy:.4f} N-mm") print(f"\n Top 5 elements:") for eid, energy in result['top_elements'][:5]: print(f" {eid}: {energy:.4f} N-mm") print(f"\n Total elements: {result['num_elements']}") except ValueError as e: print(f"Error: {e}") else: print(f"Usage: python {sys.argv[0]} ")