279 lines
9.4 KiB
Python
279 lines
9.4 KiB
Python
|
|
"""
|
||
|
|
Robust OP2 Extraction - Handles pyNastran FATAL flag issues gracefully.
|
||
|
|
|
||
|
|
This module provides a more robust OP2 extraction that:
|
||
|
|
1. Catches pyNastran FATAL flag exceptions
|
||
|
|
2. Checks if eigenvalues were actually extracted despite the flag
|
||
|
|
3. Falls back to F06 extraction if OP2 fails
|
||
|
|
4. Logs detailed failure information
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
from optimization_engine.op2_extractor import robust_extract_first_frequency
|
||
|
|
|
||
|
|
frequency = robust_extract_first_frequency(
|
||
|
|
op2_file=Path("results.op2"),
|
||
|
|
mode_number=1,
|
||
|
|
f06_file=Path("results.f06"), # Optional fallback
|
||
|
|
verbose=True
|
||
|
|
)
|
||
|
|
"""
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Optional, Tuple
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
|
||
|
|
def robust_extract_first_frequency(
|
||
|
|
op2_file: Path,
|
||
|
|
mode_number: int = 1,
|
||
|
|
f06_file: Optional[Path] = None,
|
||
|
|
verbose: bool = False
|
||
|
|
) -> float:
|
||
|
|
"""
|
||
|
|
Robustly extract natural frequency from OP2 file, handling pyNastran issues.
|
||
|
|
|
||
|
|
This function attempts multiple strategies:
|
||
|
|
1. Standard pyNastran OP2 reading
|
||
|
|
2. Force reading with debug=False to ignore FATAL flags
|
||
|
|
3. Partial OP2 reading (extract eigenvalues even if FATAL flag exists)
|
||
|
|
4. Fallback to F06 file parsing (if provided)
|
||
|
|
|
||
|
|
Args:
|
||
|
|
op2_file: Path to OP2 output file
|
||
|
|
mode_number: Mode number to extract (1-based index)
|
||
|
|
f06_file: Optional F06 file for fallback extraction
|
||
|
|
verbose: Print detailed extraction information
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Natural frequency in Hz
|
||
|
|
|
||
|
|
Raises:
|
||
|
|
ValueError: If frequency cannot be extracted by any method
|
||
|
|
"""
|
||
|
|
from pyNastran.op2.op2 import OP2
|
||
|
|
|
||
|
|
if not op2_file.exists():
|
||
|
|
raise FileNotFoundError(f"OP2 file not found: {op2_file}")
|
||
|
|
|
||
|
|
# Strategy 1: Try standard OP2 reading
|
||
|
|
try:
|
||
|
|
if verbose:
|
||
|
|
print(f"[OP2 EXTRACT] Attempting standard read: {op2_file.name}")
|
||
|
|
|
||
|
|
model = OP2()
|
||
|
|
model.read_op2(str(op2_file))
|
||
|
|
|
||
|
|
if hasattr(model, 'eigenvalues') and len(model.eigenvalues) > 0:
|
||
|
|
frequency = _extract_frequency_from_model(model, mode_number)
|
||
|
|
if verbose:
|
||
|
|
print(f"[OP2 EXTRACT] ✓ Success (standard read): {frequency:.6f} Hz")
|
||
|
|
return frequency
|
||
|
|
else:
|
||
|
|
raise ValueError("No eigenvalues found in OP2 file")
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
if verbose:
|
||
|
|
print(f"[OP2 EXTRACT] ✗ Standard read failed: {str(e)[:100]}")
|
||
|
|
|
||
|
|
# Check if this is a FATAL flag issue
|
||
|
|
is_fatal_flag = 'FATAL' in str(e) and 'op2_reader' in str(e.__class__.__module__)
|
||
|
|
|
||
|
|
if is_fatal_flag:
|
||
|
|
# Strategy 2: Try reading with more lenient settings
|
||
|
|
if verbose:
|
||
|
|
print(f"[OP2 EXTRACT] Detected pyNastran FATAL flag issue")
|
||
|
|
print(f"[OP2 EXTRACT] Attempting partial extraction...")
|
||
|
|
|
||
|
|
try:
|
||
|
|
model = OP2()
|
||
|
|
# Try to read with debug=False and skip_undefined_matrices=True
|
||
|
|
model.read_op2(
|
||
|
|
str(op2_file),
|
||
|
|
debug=False,
|
||
|
|
skip_undefined_matrices=True
|
||
|
|
)
|
||
|
|
|
||
|
|
# Check if eigenvalues were extracted despite FATAL
|
||
|
|
if hasattr(model, 'eigenvalues') and len(model.eigenvalues) > 0:
|
||
|
|
frequency = _extract_frequency_from_model(model, mode_number)
|
||
|
|
if verbose:
|
||
|
|
print(f"[OP2 EXTRACT] ✓ Success (lenient mode): {frequency:.6f} Hz")
|
||
|
|
print(f"[OP2 EXTRACT] Note: pyNastran reported FATAL but data is valid!")
|
||
|
|
return frequency
|
||
|
|
|
||
|
|
except Exception as e2:
|
||
|
|
if verbose:
|
||
|
|
print(f"[OP2 EXTRACT] ✗ Lenient read also failed: {str(e2)[:100]}")
|
||
|
|
|
||
|
|
# Strategy 3: Fallback to F06 parsing
|
||
|
|
if f06_file and f06_file.exists():
|
||
|
|
if verbose:
|
||
|
|
print(f"[OP2 EXTRACT] Falling back to F06 extraction: {f06_file.name}")
|
||
|
|
|
||
|
|
try:
|
||
|
|
frequency = extract_frequency_from_f06(f06_file, mode_number, verbose=verbose)
|
||
|
|
if verbose:
|
||
|
|
print(f"[OP2 EXTRACT] ✓ Success (F06 fallback): {frequency:.6f} Hz")
|
||
|
|
return frequency
|
||
|
|
|
||
|
|
except Exception as e3:
|
||
|
|
if verbose:
|
||
|
|
print(f"[OP2 EXTRACT] ✗ F06 extraction failed: {str(e3)}")
|
||
|
|
|
||
|
|
# All strategies failed
|
||
|
|
raise ValueError(
|
||
|
|
f"Could not extract frequency from OP2 file: {op2_file.name}. "
|
||
|
|
f"Original error: {str(e)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _extract_frequency_from_model(model, mode_number: int) -> float:
|
||
|
|
"""Extract frequency from loaded OP2 model."""
|
||
|
|
if not hasattr(model, 'eigenvalues') or len(model.eigenvalues) == 0:
|
||
|
|
raise ValueError("No eigenvalues found in model")
|
||
|
|
|
||
|
|
# Get first subcase
|
||
|
|
subcase = list(model.eigenvalues.keys())[0]
|
||
|
|
eig_obj = model.eigenvalues[subcase]
|
||
|
|
|
||
|
|
# Check if mode exists
|
||
|
|
if mode_number > len(eig_obj.eigenvalues):
|
||
|
|
raise ValueError(
|
||
|
|
f"Mode {mode_number} not found. "
|
||
|
|
f"Only {len(eig_obj.eigenvalues)} modes available"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Extract eigenvalue and convert to frequency
|
||
|
|
eigenvalue = eig_obj.eigenvalues[mode_number - 1]
|
||
|
|
angular_freq = np.sqrt(abs(eigenvalue)) # Use abs to handle numerical precision issues
|
||
|
|
frequency_hz = angular_freq / (2 * np.pi)
|
||
|
|
|
||
|
|
return float(frequency_hz)
|
||
|
|
|
||
|
|
|
||
|
|
def extract_frequency_from_f06(
|
||
|
|
f06_file: Path,
|
||
|
|
mode_number: int = 1,
|
||
|
|
verbose: bool = False
|
||
|
|
) -> float:
|
||
|
|
"""
|
||
|
|
Extract natural frequency from F06 text file (fallback method).
|
||
|
|
|
||
|
|
Parses the F06 file to find eigenvalue results table and extracts frequency.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
f06_file: Path to F06 output file
|
||
|
|
mode_number: Mode number to extract (1-based index)
|
||
|
|
verbose: Print extraction details
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Natural frequency in Hz
|
||
|
|
|
||
|
|
Raises:
|
||
|
|
ValueError: If frequency cannot be found in F06
|
||
|
|
"""
|
||
|
|
if not f06_file.exists():
|
||
|
|
raise FileNotFoundError(f"F06 file not found: {f06_file}")
|
||
|
|
|
||
|
|
with open(f06_file, 'r', encoding='latin-1', errors='ignore') as f:
|
||
|
|
content = f.read()
|
||
|
|
|
||
|
|
# Look for eigenvalue table
|
||
|
|
# Nastran F06 format has eigenvalue results like:
|
||
|
|
# R E A L E I G E N V A L U E S
|
||
|
|
# MODE EXTRACTION EIGENVALUE RADIANS CYCLES GENERALIZED GENERALIZED
|
||
|
|
# NO. ORDER MASS STIFFNESS
|
||
|
|
# 1 1 -6.602743E+04 2.569656E+02 4.089338E+01 1.000000E+00 6.602743E+04
|
||
|
|
|
||
|
|
lines = content.split('\n')
|
||
|
|
|
||
|
|
# Find eigenvalue table
|
||
|
|
eigenvalue_section_start = None
|
||
|
|
for i, line in enumerate(lines):
|
||
|
|
if 'R E A L E I G E N V A L U E S' in line:
|
||
|
|
eigenvalue_section_start = i
|
||
|
|
break
|
||
|
|
|
||
|
|
if eigenvalue_section_start is None:
|
||
|
|
raise ValueError("Eigenvalue table not found in F06 file")
|
||
|
|
|
||
|
|
# Parse eigenvalue table (starts a few lines after header)
|
||
|
|
for i in range(eigenvalue_section_start + 3, min(eigenvalue_section_start + 100, len(lines))):
|
||
|
|
line = lines[i].strip()
|
||
|
|
|
||
|
|
if not line or line.startswith('1'): # Page break
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Parse line with mode data
|
||
|
|
parts = line.split()
|
||
|
|
if len(parts) >= 5:
|
||
|
|
try:
|
||
|
|
mode_num = int(parts[0])
|
||
|
|
if mode_num == mode_number:
|
||
|
|
# Frequency is in column 5 (CYCLES)
|
||
|
|
frequency = float(parts[4])
|
||
|
|
if verbose:
|
||
|
|
print(f"[F06 EXTRACT] Found mode {mode_num}: {frequency:.6f} Hz")
|
||
|
|
return frequency
|
||
|
|
except (ValueError, IndexError):
|
||
|
|
continue
|
||
|
|
|
||
|
|
raise ValueError(f"Mode {mode_number} not found in F06 eigenvalue table")
|
||
|
|
|
||
|
|
|
||
|
|
def validate_op2_file(op2_file: Path, f06_file: Optional[Path] = None) -> Tuple[bool, str]:
|
||
|
|
"""
|
||
|
|
Validate if an OP2 file contains usable eigenvalue data.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
op2_file: Path to OP2 file
|
||
|
|
f06_file: Optional F06 file for cross-reference
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
(is_valid, message): Tuple of validation status and explanation
|
||
|
|
"""
|
||
|
|
if not op2_file.exists():
|
||
|
|
return False, f"OP2 file does not exist: {op2_file}"
|
||
|
|
|
||
|
|
if op2_file.stat().st_size == 0:
|
||
|
|
return False, "OP2 file is empty"
|
||
|
|
|
||
|
|
# Try to extract first frequency
|
||
|
|
try:
|
||
|
|
frequency = robust_extract_first_frequency(
|
||
|
|
op2_file,
|
||
|
|
mode_number=1,
|
||
|
|
f06_file=f06_file,
|
||
|
|
verbose=False
|
||
|
|
)
|
||
|
|
return True, f"Valid OP2 file (first frequency: {frequency:.6f} Hz)"
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
return False, f"Cannot extract data from OP2: {str(e)}"
|
||
|
|
|
||
|
|
|
||
|
|
# Convenience function (same signature as old function for backward compatibility)
|
||
|
|
def extract_first_frequency(op2_file: Path, mode_number: int = 1) -> float:
|
||
|
|
"""
|
||
|
|
Extract first natural frequency (backward compatible with old function).
|
||
|
|
|
||
|
|
This is the simple version - just use robust_extract_first_frequency directly
|
||
|
|
for more control.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
op2_file: Path to OP2 file
|
||
|
|
mode_number: Mode number (1-based)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Frequency in Hz
|
||
|
|
"""
|
||
|
|
# Try to find F06 file in same directory
|
||
|
|
f06_file = op2_file.with_suffix('.f06')
|
||
|
|
|
||
|
|
return robust_extract_first_frequency(
|
||
|
|
op2_file,
|
||
|
|
mode_number=mode_number,
|
||
|
|
f06_file=f06_file if f06_file.exists() else None,
|
||
|
|
verbose=False
|
||
|
|
)
|