Files
Atomizer/optimization_engine/extractors/extract_temperature.py

468 lines
17 KiB
Python
Raw Normal View History

"""
Temperature Extractor for Thermal Analysis Results
===================================================
Phase 3 Task 3.1 - NX Open Automation Roadmap
Extracts nodal temperature results from Nastran OP2 files for thermal optimization.
Usage:
from optimization_engine.extractors.extract_temperature import extract_temperature
result = extract_temperature("path/to/thermal.op2", subcase=1)
print(f"Max temperature: {result['max_temperature']} K")
Supports:
- SOL 153 (Steady-State Heat Transfer)
- SOL 159 (Transient Heat Transfer)
- Thermal subcases in coupled analyses
"""
import numpy as np
from pathlib import Path
from typing import Dict, Any, Optional, List, Union
import logging
logger = logging.getLogger(__name__)
def extract_temperature(
op2_file: Union[str, Path],
subcase: int = 1,
nodes: Optional[List[int]] = None,
return_field: bool = False
) -> Dict[str, Any]:
"""
Extract nodal temperatures from thermal analysis OP2 file.
Args:
op2_file: Path to the OP2 results file
subcase: Subcase number to extract (default: 1)
nodes: Optional list of specific node IDs to extract.
If None, extracts all nodes.
return_field: If True, include full temperature field in result
Returns:
dict: {
'success': bool,
'max_temperature': float (K or °C depending on model units),
'min_temperature': float,
'avg_temperature': float,
'max_node_id': int (node with max temperature),
'min_node_id': int (node with min temperature),
'node_count': int,
'temperatures': dict (node_id: temp) - only if return_field=True,
'unit': str ('K' or 'C'),
'subcase': int,
'error': str or None
}
Example:
>>> result = extract_temperature("thermal_analysis.op2", subcase=1)
>>> if result['success']:
... print(f"Max temp: {result['max_temperature']:.1f} K at node {result['max_node_id']}")
... print(f"Temperature range: {result['min_temperature']:.1f} - {result['max_temperature']:.1f} K")
"""
op2_path = Path(op2_file)
if not op2_path.exists():
return {
'success': False,
'error': f"OP2 file not found: {op2_path}",
'max_temperature': None,
'min_temperature': None,
'avg_temperature': None,
'max_node_id': None,
'min_node_id': None,
'node_count': 0,
'subcase': subcase
}
try:
from pyNastran.op2.op2 import read_op2
# Read OP2 with minimal output
op2 = read_op2(str(op2_path), load_geometry=False, debug=False, log=None)
# Check for temperature results
# pyNastran stores temperatures in different attributes depending on analysis type
temperatures = None
# Method 1: Check temperatures attribute (SOL 153/159)
if hasattr(op2, 'temperatures') and op2.temperatures:
if subcase in op2.temperatures:
temp_data = op2.temperatures[subcase]
temperatures = _extract_from_table(temp_data, nodes)
logger.debug(f"Found temperatures in op2.temperatures[{subcase}]")
# Method 2: Check thermal load results (alternative storage)
if temperatures is None and hasattr(op2, 'thermal_load_vectors'):
if subcase in op2.thermal_load_vectors:
temp_data = op2.thermal_load_vectors[subcase]
temperatures = _extract_from_table(temp_data, nodes)
logger.debug(f"Found temperatures in op2.thermal_load_vectors[{subcase}]")
# Method 3: Check displacement as temperature (some solvers store temp in disp)
# In thermal analysis, "displacement" can actually be temperature
if temperatures is None and hasattr(op2, 'displacements') and op2.displacements:
if subcase in op2.displacements:
disp_data = op2.displacements[subcase]
# Check if this is actually temperature data
# Temperature data typically has only 1 DOF (scalar field)
if hasattr(disp_data, 'data'):
data = disp_data.data
if len(data.shape) >= 2:
# For thermal, we expect scalar temperature at each node
# Column 0 of translational data contains temperature
temps_array = data[0, :, 0] if len(data.shape) == 3 else data[:, 0]
node_ids = disp_data.node_gridtype[:, 0]
temperatures = {int(nid): float(temps_array[i])
for i, nid in enumerate(node_ids)
if nodes is None or nid in nodes}
logger.debug(f"Found temperatures in op2.displacements[{subcase}] (thermal mode)")
if temperatures is None or len(temperatures) == 0:
# List available subcases for debugging
available_subcases = []
if hasattr(op2, 'temperatures') and op2.temperatures:
available_subcases.extend(list(op2.temperatures.keys()))
return {
'success': False,
'error': f"No temperature results found for subcase {subcase}. "
f"Available subcases: {available_subcases}",
'max_temperature': None,
'min_temperature': None,
'avg_temperature': None,
'max_node_id': None,
'min_node_id': None,
'node_count': 0,
'subcase': subcase
}
# Compute statistics
temp_values = np.array(list(temperatures.values()))
temp_nodes = np.array(list(temperatures.keys()))
max_idx = np.argmax(temp_values)
min_idx = np.argmin(temp_values)
result = {
'success': True,
'error': None,
'max_temperature': float(temp_values[max_idx]),
'min_temperature': float(temp_values[min_idx]),
'avg_temperature': float(np.mean(temp_values)),
'std_temperature': float(np.std(temp_values)),
'max_node_id': int(temp_nodes[max_idx]),
'min_node_id': int(temp_nodes[min_idx]),
'node_count': len(temperatures),
'unit': 'K', # Nastran typically uses Kelvin
'subcase': subcase
}
if return_field:
result['temperatures'] = temperatures
return result
except ImportError:
return {
'success': False,
'error': "pyNastran not installed. Install with: pip install pyNastran",
'max_temperature': None,
'min_temperature': None,
'avg_temperature': None,
'max_node_id': None,
'min_node_id': None,
'node_count': 0,
'subcase': subcase
}
except Exception as e:
logger.exception(f"Error extracting temperature from {op2_path}")
return {
'success': False,
'error': str(e),
'max_temperature': None,
'min_temperature': None,
'avg_temperature': None,
'max_node_id': None,
'min_node_id': None,
'node_count': 0,
'subcase': subcase
}
def _extract_from_table(temp_data, nodes: Optional[List[int]] = None) -> Dict[int, float]:
"""Extract temperature values from a pyNastran result table."""
temperatures = {}
if hasattr(temp_data, 'data') and hasattr(temp_data, 'node_gridtype'):
data = temp_data.data
node_ids = temp_data.node_gridtype[:, 0]
# Data shape is typically (ntimes, nnodes, ncomponents)
# For temperature, we want the first component
if len(data.shape) == 3:
temp_values = data[0, :, 0] # First time step, all nodes, first component
elif len(data.shape) == 2:
temp_values = data[:, 0]
else:
temp_values = data
for i, nid in enumerate(node_ids):
if nodes is None or nid in nodes:
temperatures[int(nid)] = float(temp_values[i])
return temperatures
def extract_temperature_gradient(
op2_file: Union[str, Path],
subcase: int = 1,
method: str = 'nodal_difference'
) -> Dict[str, Any]:
"""
Extract temperature gradients from thermal analysis.
Computes temperature gradients based on nodal temperature differences.
This is useful for identifying thermal stress hot spots.
Args:
op2_file: Path to the OP2 results file
subcase: Subcase number
method: Gradient calculation method:
- 'nodal_difference': Max temperature difference between adjacent nodes
- 'element_based': Gradient within elements (requires mesh connectivity)
Returns:
dict: {
'success': bool,
'max_gradient': float (K/mm or temperature units/length),
'avg_gradient': float,
'temperature_range': float (max - min temperature),
'gradient_location': tuple (node_id_hot, node_id_cold),
'error': str or None
}
Note:
For accurate gradients, element-based calculation requires mesh connectivity
which may not be available in all OP2 files. The nodal_difference method
provides an approximation based on temperature range.
"""
# First extract temperatures
temp_result = extract_temperature(op2_file, subcase=subcase, return_field=True)
if not temp_result['success']:
return {
'success': False,
'error': temp_result['error'],
'max_gradient': None,
'avg_gradient': None,
'temperature_range': None,
'gradient_location': None
}
temperatures = temp_result.get('temperatures', {})
if len(temperatures) < 2:
return {
'success': False,
'error': "Need at least 2 nodes to compute gradient",
'max_gradient': None,
'avg_gradient': None,
'temperature_range': None,
'gradient_location': None
}
# Compute temperature range (proxy for max gradient without mesh)
temp_range = temp_result['max_temperature'] - temp_result['min_temperature']
# For nodal_difference method, we report the temperature range
# True gradient computation would require mesh connectivity
return {
'success': True,
'error': None,
'max_gradient': temp_range, # Simplified - actual gradient needs mesh
'avg_gradient': temp_range / 2, # Rough estimate
'temperature_range': temp_range,
'gradient_location': (temp_result['max_node_id'], temp_result['min_node_id']),
'max_temperature': temp_result['max_temperature'],
'min_temperature': temp_result['min_temperature'],
'note': "Gradient approximated from temperature range. "
"Accurate gradient requires mesh connectivity."
}
def extract_heat_flux(
op2_file: Union[str, Path],
subcase: int = 1,
element_type: str = 'all'
) -> Dict[str, Any]:
"""
Extract element heat flux from thermal analysis OP2 file.
Args:
op2_file: Path to the OP2 results file
subcase: Subcase number
element_type: Element type to extract ('all', 'ctetra', 'chexa', etc.)
Returns:
dict: {
'success': bool,
'max_heat_flux': float (W/mm² or model units),
'min_heat_flux': float,
'avg_heat_flux': float,
'max_element_id': int,
'element_count': int,
'unit': str,
'error': str or None
}
"""
op2_path = Path(op2_file)
if not op2_path.exists():
return {
'success': False,
'error': f"OP2 file not found: {op2_path}",
'max_heat_flux': None,
'min_heat_flux': None,
'avg_heat_flux': None,
'max_element_id': None,
'element_count': 0,
'subcase': subcase
}
try:
from pyNastran.op2.op2 import read_op2
op2 = read_op2(str(op2_path), load_geometry=False, debug=False, log=None)
# Check for heat flux results
# pyNastran stores thermal flux in chbdyg_thermal_load or similar
heat_flux_data = None
# Check various thermal flux attributes
flux_attrs = [
'chbdyg_thermal_load',
'chbdye_thermal_load',
'chbdyp_thermal_load',
'thermalLoad_CONV',
'thermalLoad_CHBDY'
]
for attr in flux_attrs:
if hasattr(op2, attr):
data = getattr(op2, attr)
if data and subcase in data:
heat_flux_data = data[subcase]
logger.debug(f"Found heat flux in op2.{attr}[{subcase}]")
break
if heat_flux_data is None:
return {
'success': False,
'error': f"No heat flux results found for subcase {subcase}. "
"Heat flux output may not be requested in the analysis.",
'max_heat_flux': None,
'min_heat_flux': None,
'avg_heat_flux': None,
'max_element_id': None,
'element_count': 0,
'subcase': subcase
}
# Extract flux values
if hasattr(heat_flux_data, 'data'):
data = heat_flux_data.data
element_ids = heat_flux_data.element if hasattr(heat_flux_data, 'element') else []
# Flux magnitude
if len(data.shape) == 3:
flux_values = np.linalg.norm(data[0, :, :], axis=1)
else:
flux_values = np.abs(data[:, 0]) if len(data.shape) == 2 else data
max_idx = np.argmax(flux_values)
return {
'success': True,
'error': None,
'max_heat_flux': float(np.max(flux_values)),
'min_heat_flux': float(np.min(flux_values)),
'avg_heat_flux': float(np.mean(flux_values)),
'max_element_id': int(element_ids[max_idx]) if len(element_ids) > max_idx else None,
'element_count': len(flux_values),
'unit': 'W/mm²',
'subcase': subcase
}
return {
'success': False,
'error': "Could not parse heat flux data format",
'max_heat_flux': None,
'min_heat_flux': None,
'avg_heat_flux': None,
'max_element_id': None,
'element_count': 0,
'subcase': subcase
}
except Exception as e:
logger.exception(f"Error extracting heat flux from {op2_path}")
return {
'success': False,
'error': str(e),
'max_heat_flux': None,
'min_heat_flux': None,
'avg_heat_flux': None,
'max_element_id': None,
'element_count': 0,
'subcase': subcase
}
# Convenience function for optimization constraints
def get_max_temperature(op2_file: Union[str, Path], subcase: int = 1) -> float:
"""
Get maximum temperature from OP2 file.
Convenience function for use in optimization constraints.
Returns inf if extraction fails.
Args:
op2_file: Path to OP2 file
subcase: Subcase number
Returns:
float: Maximum temperature or inf on failure
"""
result = extract_temperature(op2_file, subcase=subcase)
if result['success']:
return result['max_temperature']
else:
logger.warning(f"Temperature extraction failed: {result['error']}")
return float('inf')
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
op2_file = sys.argv[1]
subcase = int(sys.argv[2]) if len(sys.argv) > 2 else 1
print(f"Extracting temperature from: {op2_file}")
result = extract_temperature(op2_file, subcase=subcase)
if result['success']:
print(f"\n=== Temperature Results (Subcase {subcase}) ===")
print(f"Max temperature: {result['max_temperature']:.2f} {result['unit']} (node {result['max_node_id']})")
print(f"Min temperature: {result['min_temperature']:.2f} {result['unit']} (node {result['min_node_id']})")
print(f"Avg temperature: {result['avg_temperature']:.2f} {result['unit']}")
print(f"Node count: {result['node_count']}")
else:
print(f"Error: {result['error']}")
else:
print("Usage: python extract_temperature.py <op2_file> [subcase]")