Files
Atomizer/optimization_engine/comprehensive_results_analyzer.py

394 lines
13 KiB
Python
Raw Normal View History

feat: Add centralized configuration system and Phase 3.2 enhancements Major Features Added: 1. Centralized Configuration System (config.py) - Single source of truth for all NX and environment paths - Change NX version in ONE place: NX_VERSION = "2412" - Change Python environment in ONE place: PYTHON_ENV_NAME = "atomizer" - Automatic path derivation and validation - Helper functions: get_nx_journal_command() - Future-proof: Easy to upgrade when NX 2506+ released 2. NX Path Corrections (Critical Fix) - Fixed all incorrect Simcenter3D_2412 references to NX2412 - Updated nx_updater.py to use config.NX_RUN_JOURNAL - Updated dashboard/api/app.py to use config.NX_RUN_JOURNAL - Corrected material library path to NX2412/UGII/materials - All files now use correct NX2412 installation 3. NX Expression Import System - Dual-method expression gathering (.exp export + binary parsing) - Robust handling of all NX expression types - Support for formulas, units, and dependencies - Documented in docs/NX_EXPRESSION_IMPORT_SYSTEM.md 4. Study Management & Analysis Tools - StudyCreator: Unified interface for study/substudy creation - BenchmarkingSubstudy: Automated baseline analysis - ComprehensiveResultsAnalyzer: Multi-result extraction from .op2 - Expression extractor generator (LLM-powered) 5. 50-Trial Beam Optimization Complete - Full optimization results documented - Best design: 23.1% improvement over baseline - Comprehensive analysis with plots and insights - Results in studies/simple_beam_optimization/ Documentation Updates: - docs/SYSTEM_CONFIGURATION.md - System paths and validation - docs/QUICK_CONFIG_REFERENCE.md - Quick config change guide - docs/NX_EXPRESSION_IMPORT_SYSTEM.md - Expression import details - docs/OPTIMIZATION_WORKFLOW.md - Complete workflow guide - Updated README.md with NX2412 paths Files Modified: - config.py (NEW) - Central configuration system - optimization_engine/nx_updater.py - Now uses config - dashboard/api/app.py - Now uses config - optimization_engine/study_creator.py - Enhanced features - optimization_engine/benchmarking_substudy.py - New analyzer - optimization_engine/comprehensive_results_analyzer.py - Multi-result extraction - optimization_engine/result_extractors/generated/extract_expression.py - Generated extractor Cleanup: - Removed all temporary test files - Removed migration scripts (no longer needed) - Clean production-ready codebase Strategic Impact: - Configuration maintenance time: reduced from hours to seconds - Path consistency: 100% enforced across codebase - Future NX upgrades: Edit ONE variable in config.py - Foundation for Phase 3.2 Integration completion 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 14:36:00 -05:00
"""
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}")