""" Comprehensive Results Analyzer Performs thorough introspection of OP2, F06, and other Nastran output files to discover ALL available results, not just what we expect. This helps ensure we don't miss important data that's actually in the output files. """ from pathlib import Path from typing import Dict, Any, List, Optional import json from dataclasses import dataclass, asdict from pyNastran.op2.op2 import OP2 @dataclass class OP2Contents: """Complete inventory of OP2 file contents.""" file_path: str subcases: List[int] # Displacement results displacement_available: bool displacement_subcases: List[int] # Stress results (by element type) stress_results: Dict[str, List[int]] # element_type -> [subcases] # Strain results (by element type) strain_results: Dict[str, List[int]] # Force results force_results: Dict[str, List[int]] # Other results other_results: Dict[str, Any] # Grid point forces/stresses grid_point_forces: List[int] spc_forces: List[int] mpc_forces: List[int] # Summary total_result_types: int element_types_with_results: List[str] @dataclass class F06Contents: """Complete inventory of F06 file contents.""" file_path: str has_displacement: bool has_stress: bool has_strain: bool has_forces: bool element_types_found: List[str] error_messages: List[str] warning_messages: List[str] class ComprehensiveResultsAnalyzer: """ Analyzes ALL Nastran output files to discover available results. This is much more thorough than just checking expected results. """ def __init__(self, output_dir: Path): """ Initialize analyzer. Args: output_dir: Directory containing Nastran output files (.op2, .f06, etc.) """ self.output_dir = Path(output_dir) def analyze_op2(self, op2_file: Path) -> OP2Contents: """ Comprehensively analyze OP2 file contents. Args: op2_file: Path to OP2 file Returns: OP2Contents with complete inventory """ print(f"\n[OP2 ANALYSIS] Reading: {op2_file.name}") model = OP2() model.read_op2(str(op2_file)) # Discover all subcases all_subcases = set() # Check displacement displacement_available = hasattr(model, 'displacements') and len(model.displacements) > 0 displacement_subcases = list(model.displacements.keys()) if displacement_available else [] all_subcases.update(displacement_subcases) print(f" Displacement: {'YES' if displacement_available else 'NO'}") if displacement_subcases: print(f" Subcases: {displacement_subcases}") # Check ALL stress results by scanning attributes stress_results = {} element_types_with_stress = [] # List of known stress attribute names (safer than scanning all attributes) stress_attrs = [ 'cquad4_stress', 'ctria3_stress', 'ctetra_stress', 'chexa_stress', 'cpenta_stress', 'cbar_stress', 'cbeam_stress', 'crod_stress', 'conrod_stress', 'ctube_stress', 'cshear_stress', 'cbush_stress', 'cgap_stress', 'celas1_stress', 'celas2_stress', 'celas3_stress', 'celas4_stress' ] for attr_name in stress_attrs: if hasattr(model, attr_name): try: stress_obj = getattr(model, attr_name) if isinstance(stress_obj, dict) and len(stress_obj) > 0: element_type = attr_name.replace('_stress', '') subcases = list(stress_obj.keys()) stress_results[element_type] = subcases element_types_with_stress.append(element_type) all_subcases.update(subcases) print(f" Stress [{element_type}]: YES") print(f" Subcases: {subcases}") except Exception as e: # Skip attributes that cause errors pass if not stress_results: print(f" Stress: NO stress results found") # Check ALL strain results strain_results = {} strain_attrs = [attr.replace('_stress', '_strain') for attr in stress_attrs] for attr_name in strain_attrs: if hasattr(model, attr_name): try: strain_obj = getattr(model, attr_name) if isinstance(strain_obj, dict) and len(strain_obj) > 0: element_type = attr_name.replace('_strain', '') subcases = list(strain_obj.keys()) strain_results[element_type] = subcases all_subcases.update(subcases) print(f" Strain [{element_type}]: YES") print(f" Subcases: {subcases}") except Exception as e: pass if not strain_results: print(f" Strain: NO strain results found") # Check ALL force results force_results = {} force_attrs = [attr.replace('_stress', '_force') for attr in stress_attrs] for attr_name in force_attrs: if hasattr(model, attr_name): try: force_obj = getattr(model, attr_name) if isinstance(force_obj, dict) and len(force_obj) > 0: element_type = attr_name.replace('_force', '') subcases = list(force_obj.keys()) force_results[element_type] = subcases all_subcases.update(subcases) print(f" Force [{element_type}]: YES") print(f" Subcases: {subcases}") except Exception as e: pass if not force_results: print(f" Force: NO force results found") # Check grid point forces grid_point_forces = list(model.grid_point_forces.keys()) if hasattr(model, 'grid_point_forces') else [] if grid_point_forces: print(f" Grid Point Forces: YES") print(f" Subcases: {grid_point_forces}") all_subcases.update(grid_point_forces) # Check SPC/MPC forces spc_forces = list(model.spc_forces.keys()) if hasattr(model, 'spc_forces') else [] mpc_forces = list(model.mpc_forces.keys()) if hasattr(model, 'mpc_forces') else [] if spc_forces: print(f" SPC Forces: YES") print(f" Subcases: {spc_forces}") all_subcases.update(spc_forces) if mpc_forces: print(f" MPC Forces: YES") print(f" Subcases: {mpc_forces}") all_subcases.update(mpc_forces) # Check for other interesting results other_results = {} interesting_attrs = ['eigenvalues', 'eigenvectors', 'thermal_load_vectors', 'load_vectors', 'contact', 'glue', 'slide_lines'] for attr_name in interesting_attrs: if hasattr(model, attr_name): obj = getattr(model, attr_name) if obj and (isinstance(obj, dict) and len(obj) > 0) or (not isinstance(obj, dict)): other_results[attr_name] = str(type(obj)) print(f" {attr_name}: YES") # Collect all element types that have any results all_element_types = set() all_element_types.update(stress_results.keys()) all_element_types.update(strain_results.keys()) all_element_types.update(force_results.keys()) total_result_types = ( len(stress_results) + len(strain_results) + len(force_results) + (1 if displacement_available else 0) + (1 if grid_point_forces else 0) + (1 if spc_forces else 0) + (1 if mpc_forces else 0) + len(other_results) ) print(f"\n SUMMARY:") print(f" Total subcases: {len(all_subcases)}") print(f" Total result types: {total_result_types}") print(f" Element types with results: {sorted(all_element_types)}") return OP2Contents( file_path=str(op2_file), subcases=sorted(all_subcases), displacement_available=displacement_available, displacement_subcases=displacement_subcases, stress_results=stress_results, strain_results=strain_results, force_results=force_results, other_results=other_results, grid_point_forces=grid_point_forces, spc_forces=spc_forces, mpc_forces=mpc_forces, total_result_types=total_result_types, element_types_with_results=sorted(all_element_types) ) def analyze_f06(self, f06_file: Path) -> F06Contents: """ Analyze F06 file for available results. Args: f06_file: Path to F06 file Returns: F06Contents with inventory """ print(f"\n[F06 ANALYSIS] Reading: {f06_file.name}") if not f06_file.exists(): print(f" F06 file not found") return F06Contents( file_path=str(f06_file), has_displacement=False, has_stress=False, has_strain=False, has_forces=False, element_types_found=[], error_messages=[], warning_messages=[] ) # Read F06 file with open(f06_file, 'r', encoding='latin-1', errors='ignore') as f: content = f.read() # Search for key sections has_displacement = 'D I S P L A C E M E N T' in content has_stress = 'S T R E S S E S' in content has_strain = 'S T R A I N S' in content has_forces = 'F O R C E S' in content print(f" Displacement: {'YES' if has_displacement else 'NO'}") print(f" Stress: {'YES' if has_stress else 'NO'}") print(f" Strain: {'YES' if has_strain else 'NO'}") print(f" Forces: {'YES' if has_forces else 'NO'}") # Find element types mentioned element_keywords = ['CQUAD4', 'CTRIA3', 'CTETRA', 'CHEXA', 'CPENTA', 'CBAR', 'CBEAM', 'CROD'] element_types_found = [] for elem_type in element_keywords: if elem_type in content: element_types_found.append(elem_type) if element_types_found: print(f" Element types: {element_types_found}") # Extract errors and warnings error_messages = [] warning_messages = [] for line in content.split('\n'): line_upper = line.upper() if 'ERROR' in line_upper or 'FATAL' in line_upper: error_messages.append(line.strip()) elif 'WARNING' in line_upper or 'WARN' in line_upper: warning_messages.append(line.strip()) if error_messages: print(f" Errors found: {len(error_messages)}") for err in error_messages[:5]: # Show first 5 print(f" {err}") if warning_messages: print(f" Warnings found: {len(warning_messages)}") for warn in warning_messages[:5]: # Show first 5 print(f" {warn}") return F06Contents( file_path=str(f06_file), has_displacement=has_displacement, has_stress=has_stress, has_strain=has_strain, has_forces=has_forces, element_types_found=element_types_found, error_messages=error_messages[:20], # Keep first 20 warning_messages=warning_messages[:20] ) def analyze_all(self, op2_pattern: str = "*.op2", f06_pattern: str = "*.f06") -> Dict[str, Any]: """ Analyze all OP2 and F06 files in directory. Args: op2_pattern: Glob pattern for OP2 files f06_pattern: Glob pattern for F06 files Returns: Dict with complete analysis results """ print("="*80) print("COMPREHENSIVE NASTRAN RESULTS ANALYSIS") print("="*80) print(f"\nDirectory: {self.output_dir}") results = { 'directory': str(self.output_dir), 'op2_files': [], 'f06_files': [] } # Find and analyze all OP2 files op2_files = list(self.output_dir.glob(op2_pattern)) print(f"\nFound {len(op2_files)} OP2 file(s)") for op2_file in op2_files: op2_contents = self.analyze_op2(op2_file) results['op2_files'].append(asdict(op2_contents)) # Find and analyze all F06 files f06_files = list(self.output_dir.glob(f06_pattern)) print(f"\nFound {len(f06_files)} F06 file(s)") for f06_file in f06_files: f06_contents = self.analyze_f06(f06_file) results['f06_files'].append(asdict(f06_contents)) print("\n" + "="*80) print("ANALYSIS COMPLETE") print("="*80) return results if __name__ == '__main__': import sys if len(sys.argv) > 1: output_dir = Path(sys.argv[1]) else: output_dir = Path.cwd() analyzer = ComprehensiveResultsAnalyzer(output_dir) results = analyzer.analyze_all() # Save results to JSON output_file = output_dir / "comprehensive_results_analysis.json" with open(output_file, 'w', encoding='utf-8') as f: json.dump(results, f, indent=2) print(f"\nResults saved to: {output_file}")