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>
394 lines
13 KiB
Python
394 lines
13 KiB
Python
"""
|
|
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}")
|