refactor: Major project cleanup and reorganization
## Removed Duplicate Directories - Deleted old `dashboard/` (replaced by atomizer-dashboard) - Deleted old `mcp_server/` Python tools (moved model_discovery to optimization_engine) - Deleted `tests/mcp_server/` (obsolete tests) - Deleted `launch_dashboard.bat` (old launcher) ## Consolidated Code - Moved `mcp_server/tools/model_discovery.py` to `optimization_engine/model_discovery/` - Updated import in `optimization_config_builder.py` - Deleted stub `extract_mass.py` (use extract_mass_from_bdf instead) - Deleted unused `intelligent_setup.py` and `hybrid_study_creator.py` - Archived `result_extractors/` to `archive/deprecated/` ## Documentation Cleanup - Deleted deprecated `docs/06_PROTOCOLS_DETAILED/` (14 files) - Archived dated dev docs to `docs/08_ARCHIVE/sessions/` - Archived old plans to `docs/08_ARCHIVE/plans/` - Updated `docs/protocols/README.md` with SYS_15 ## Skills Consolidation - Archived redundant study creation skills to `.claude/skills/archive/` - Kept `core/study-creation-core.md` as canonical ## Housekeeping - Updated `.gitignore` to prevent `nul` and `_dat_run*.dat` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
66
archive/deprecated/result_extractors/__init__.py
Normal file
66
archive/deprecated/result_extractors/__init__.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
Pluggable Result Extractor System
|
||||
|
||||
Base classes and implementations for extracting metrics from FEA results.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ResultExtractor(ABC):
|
||||
"""Base class for all result extractors."""
|
||||
|
||||
@abstractmethod
|
||||
def extract(self, result_files: Dict[str, Path], config: Dict[str, Any]) -> Dict[str, float]:
|
||||
"""
|
||||
Extract metrics from FEA results.
|
||||
|
||||
Args:
|
||||
result_files: Dictionary mapping file types to paths (e.g., {'op2': Path(...), 'f06': Path(...)})
|
||||
config: Extractor-specific configuration parameters
|
||||
|
||||
Returns:
|
||||
Dictionary mapping metric names to values
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def required_files(self) -> list[str]:
|
||||
"""List of required file types (e.g., ['op2'], ['f06'], etc.)."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Extractor name for registration."""
|
||||
return self.__class__.__name__.replace("Extractor", "").lower()
|
||||
|
||||
|
||||
# Registry of available extractors
|
||||
_EXTRACTOR_REGISTRY: Dict[str, type[ResultExtractor]] = {}
|
||||
|
||||
|
||||
def register_extractor(extractor_class: type[ResultExtractor]) -> type[ResultExtractor]:
|
||||
"""Decorator to register an extractor."""
|
||||
_EXTRACTOR_REGISTRY[extractor_class().name] = extractor_class
|
||||
return extractor_class
|
||||
|
||||
|
||||
def get_extractor(name: str) -> Optional[type[ResultExtractor]]:
|
||||
"""Get extractor class by name."""
|
||||
return _EXTRACTOR_REGISTRY.get(name)
|
||||
|
||||
|
||||
def list_extractors() -> list[str]:
|
||||
"""List all registered extractor names."""
|
||||
return list(_EXTRACTOR_REGISTRY.keys())
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ResultExtractor",
|
||||
"register_extractor",
|
||||
"get_extractor",
|
||||
"list_extractors",
|
||||
]
|
||||
207
archive/deprecated/result_extractors/extractors.py
Normal file
207
archive/deprecated/result_extractors/extractors.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Result Extractors
|
||||
|
||||
Wrapper functions that integrate with the optimization runner.
|
||||
These extract optimization metrics from NX Nastran result files.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import sys
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from optimization_engine.result_extractors.op2_extractor_example import (
|
||||
extract_max_displacement,
|
||||
extract_max_stress,
|
||||
extract_mass
|
||||
)
|
||||
|
||||
|
||||
def mass_extractor(result_path: Path) -> Dict[str, float]:
|
||||
"""
|
||||
Extract mass metrics for optimization.
|
||||
|
||||
Args:
|
||||
result_path: Path to .op2 file or directory containing results
|
||||
|
||||
Returns:
|
||||
Dict with 'total_mass' and other mass-related metrics
|
||||
"""
|
||||
# If result_path is a directory, find the .op2 file
|
||||
if result_path.is_dir():
|
||||
op2_files = list(result_path.glob("*.op2"))
|
||||
if not op2_files:
|
||||
raise FileNotFoundError(f"No .op2 files found in {result_path}")
|
||||
op2_path = op2_files[0] # Use first .op2 file
|
||||
else:
|
||||
op2_path = result_path
|
||||
|
||||
if not op2_path.exists():
|
||||
raise FileNotFoundError(f"Result file not found: {op2_path}")
|
||||
|
||||
result = extract_mass(op2_path)
|
||||
|
||||
# Ensure total_mass key exists
|
||||
if 'total_mass' not in result or result['total_mass'] is None:
|
||||
raise ValueError(f"Could not extract total_mass from {op2_path}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def stress_extractor(result_path: Path) -> Dict[str, float]:
|
||||
"""
|
||||
Extract stress metrics for optimization.
|
||||
|
||||
Args:
|
||||
result_path: Path to .op2 file or directory containing results
|
||||
|
||||
Returns:
|
||||
Dict with 'max_von_mises' and other stress metrics
|
||||
"""
|
||||
# If result_path is a directory, find the .op2 file
|
||||
if result_path.is_dir():
|
||||
op2_files = list(result_path.glob("*.op2"))
|
||||
if not op2_files:
|
||||
raise FileNotFoundError(f"No .op2 files found in {result_path}")
|
||||
op2_path = op2_files[0]
|
||||
else:
|
||||
op2_path = result_path
|
||||
|
||||
if not op2_path.exists():
|
||||
raise FileNotFoundError(f"Result file not found: {op2_path}")
|
||||
|
||||
result = extract_max_stress(op2_path, stress_type='von_mises')
|
||||
|
||||
# Ensure max_von_mises key exists
|
||||
if 'max_stress' in result:
|
||||
result['max_von_mises'] = result['max_stress']
|
||||
|
||||
if 'max_von_mises' not in result or result['max_von_mises'] is None:
|
||||
raise ValueError(f"Could not extract max_von_mises from {op2_path}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def displacement_extractor(result_path: Path) -> Dict[str, float]:
|
||||
"""
|
||||
Extract displacement metrics for optimization.
|
||||
|
||||
Args:
|
||||
result_path: Path to .op2 file or directory containing results
|
||||
|
||||
Returns:
|
||||
Dict with 'max_displacement' and other displacement metrics
|
||||
"""
|
||||
# If result_path is a directory, find the .op2 file
|
||||
if result_path.is_dir():
|
||||
op2_files = list(result_path.glob("*.op2"))
|
||||
if not op2_files:
|
||||
raise FileNotFoundError(f"No .op2 files found in {result_path}")
|
||||
op2_path = op2_files[0]
|
||||
else:
|
||||
op2_path = result_path
|
||||
|
||||
if not op2_path.exists():
|
||||
raise FileNotFoundError(f"Result file not found: {op2_path}")
|
||||
|
||||
result = extract_max_displacement(op2_path)
|
||||
|
||||
# Ensure max_displacement key exists
|
||||
if 'max_displacement' not in result or result['max_displacement'] is None:
|
||||
raise ValueError(f"Could not extract max_displacement from {op2_path}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def volume_extractor(result_path: Path) -> Dict[str, float]:
|
||||
"""
|
||||
Extract volume metrics for optimization.
|
||||
|
||||
Note: Volume is often not directly in OP2 files.
|
||||
This is a placeholder that could be extended to:
|
||||
- Calculate from mass and density
|
||||
- Extract from .f06 file
|
||||
- Query from NX model directly
|
||||
|
||||
Args:
|
||||
result_path: Path to result files
|
||||
|
||||
Returns:
|
||||
Dict with 'total_volume'
|
||||
"""
|
||||
# For now, estimate from mass (would need material density)
|
||||
# This is a placeholder implementation
|
||||
mass_result = mass_extractor(result_path)
|
||||
|
||||
# Assuming steel density ~7850 kg/m^3 = 7.85e-6 kg/mm^3
|
||||
# volume (mm^3) = mass (kg) / density (kg/mm^3)
|
||||
assumed_density = 7.85e-6 # kg/mm^3
|
||||
|
||||
if mass_result['total_mass']:
|
||||
total_volume = mass_result['total_mass'] / assumed_density
|
||||
else:
|
||||
total_volume = None
|
||||
|
||||
return {
|
||||
'total_volume': total_volume,
|
||||
'note': 'Volume estimated from mass using assumed density'
|
||||
}
|
||||
|
||||
|
||||
# Registry of all available extractors
|
||||
EXTRACTOR_REGISTRY = {
|
||||
'mass_extractor': mass_extractor,
|
||||
'stress_extractor': stress_extractor,
|
||||
'displacement_extractor': displacement_extractor,
|
||||
'volume_extractor': volume_extractor
|
||||
}
|
||||
|
||||
|
||||
def get_extractor(extractor_name: str):
|
||||
"""
|
||||
Get an extractor function by name.
|
||||
|
||||
Args:
|
||||
extractor_name: Name of the extractor
|
||||
|
||||
Returns:
|
||||
Extractor function
|
||||
|
||||
Raises:
|
||||
ValueError: If extractor not found
|
||||
"""
|
||||
if extractor_name not in EXTRACTOR_REGISTRY:
|
||||
available = ', '.join(EXTRACTOR_REGISTRY.keys())
|
||||
raise ValueError(f"Unknown extractor: {extractor_name}. Available: {available}")
|
||||
|
||||
return EXTRACTOR_REGISTRY[extractor_name]
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python extractors.py <path_to_op2_file>")
|
||||
print("\nAvailable extractors:")
|
||||
for name in EXTRACTOR_REGISTRY.keys():
|
||||
print(f" - {name}")
|
||||
sys.exit(1)
|
||||
|
||||
result_path = Path(sys.argv[1])
|
||||
|
||||
print("="*60)
|
||||
print("TESTING ALL EXTRACTORS")
|
||||
print("="*60)
|
||||
|
||||
for extractor_name, extractor_func in EXTRACTOR_REGISTRY.items():
|
||||
print(f"\n{extractor_name}:")
|
||||
try:
|
||||
result = extractor_func(result_path)
|
||||
for key, value in result.items():
|
||||
print(f" {key}: {value}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Extract element forces from CBAR in Z direction from OP2
|
||||
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
|
||||
|
||||
Pattern: cbar_force
|
||||
Element Type: CBAR
|
||||
Result Type: force
|
||||
API: model.cbar_force[subcase]
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import numpy as np
|
||||
from pyNastran.op2.op2 import OP2
|
||||
|
||||
|
||||
def extract_cbar_force(op2_file: Path, subcase: int = 1, direction: str = 'Z'):
|
||||
"""
|
||||
Extract forces from CBAR elements.
|
||||
|
||||
Args:
|
||||
op2_file: Path to OP2 file
|
||||
subcase: Subcase ID
|
||||
direction: Force direction ('X', 'Y', 'Z', 'axial', 'torque')
|
||||
|
||||
Returns:
|
||||
Dict with force statistics
|
||||
"""
|
||||
from pyNastran.op2.op2 import OP2
|
||||
import numpy as np
|
||||
|
||||
model = OP2()
|
||||
model.read_op2(str(op2_file))
|
||||
|
||||
if not hasattr(model, 'cbar_force'):
|
||||
raise ValueError("No CBAR force results in OP2")
|
||||
|
||||
force = model.cbar_force[subcase]
|
||||
itime = 0
|
||||
|
||||
# CBAR force data structure:
|
||||
# [bending_moment_a1, bending_moment_a2,
|
||||
# bending_moment_b1, bending_moment_b2,
|
||||
# shear1, shear2, axial, torque]
|
||||
|
||||
direction_map = {
|
||||
'shear1': 4,
|
||||
'shear2': 5,
|
||||
'axial': 6,
|
||||
'Z': 6, # Commonly axial is Z direction
|
||||
'torque': 7
|
||||
}
|
||||
|
||||
col_idx = direction_map.get(direction, direction_map.get(direction.lower(), 6))
|
||||
forces = force.data[itime, :, col_idx]
|
||||
|
||||
return {
|
||||
f'max_{direction}_force': float(np.max(np.abs(forces))),
|
||||
f'avg_{direction}_force': float(np.mean(np.abs(forces))),
|
||||
f'min_{direction}_force': float(np.min(np.abs(forces))),
|
||||
'forces_array': forces.tolist()
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example usage
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
op2_file = Path(sys.argv[1])
|
||||
result = extract_cbar_force(op2_file)
|
||||
print(f"Extraction result: {result}")
|
||||
else:
|
||||
print("Usage: python {sys.argv[0]} <op2_file>")
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Extract displacement from OP2
|
||||
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
|
||||
|
||||
Pattern: displacement
|
||||
Element Type: General
|
||||
Result Type: displacement
|
||||
API: model.displacements[subcase]
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import numpy as np
|
||||
from pyNastran.op2.op2 import OP2
|
||||
|
||||
|
||||
def extract_displacement(op2_file: Path, subcase: int = 1):
|
||||
"""Extract displacement results from OP2 file."""
|
||||
from pyNastran.op2.op2 import OP2
|
||||
import numpy as np
|
||||
|
||||
model = OP2()
|
||||
model.read_op2(str(op2_file))
|
||||
|
||||
disp = model.displacements[subcase]
|
||||
itime = 0 # static case
|
||||
|
||||
# Extract translation components
|
||||
txyz = disp.data[itime, :, :3] # [tx, ty, tz]
|
||||
|
||||
# Calculate total displacement
|
||||
total_disp = np.linalg.norm(txyz, axis=1)
|
||||
max_disp = np.max(total_disp)
|
||||
|
||||
# Get node info
|
||||
node_ids = [nid for (nid, grid_type) in disp.node_gridtype]
|
||||
max_disp_node = node_ids[np.argmax(total_disp)]
|
||||
|
||||
return {
|
||||
'max_displacement': float(max_disp),
|
||||
'max_disp_node': int(max_disp_node),
|
||||
'max_disp_x': float(np.max(np.abs(txyz[:, 0]))),
|
||||
'max_disp_y': float(np.max(np.abs(txyz[:, 1]))),
|
||||
'max_disp_z': float(np.max(np.abs(txyz[:, 2])))
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example usage
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
op2_file = Path(sys.argv[1])
|
||||
result = extract_displacement(op2_file)
|
||||
print(f"Extraction result: {result}")
|
||||
else:
|
||||
print("Usage: python {sys.argv[0]} <op2_file>")
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Extract expression value from NX .prt file
|
||||
Used for extracting computed values like mass, volume, etc.
|
||||
|
||||
This extractor reads expressions using the .exp export method for accuracy.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from optimization_engine.nx_updater import NXParameterUpdater
|
||||
|
||||
|
||||
def extract_expression(prt_file: Path, expression_name: str):
|
||||
"""
|
||||
Extract an expression value from NX .prt file.
|
||||
|
||||
Args:
|
||||
prt_file: Path to .prt file
|
||||
expression_name: Name of expression to extract (e.g., 'p173' for mass)
|
||||
|
||||
Returns:
|
||||
Dict with expression value and units
|
||||
"""
|
||||
updater = NXParameterUpdater(prt_file, backup=False)
|
||||
expressions = updater.get_all_expressions(use_exp_export=True)
|
||||
|
||||
if expression_name not in expressions:
|
||||
raise ValueError(f"Expression '{expression_name}' not found in {prt_file}")
|
||||
|
||||
expr_info = expressions[expression_name]
|
||||
|
||||
# If expression is a formula (value is None), we need to evaluate it
|
||||
# For now, we'll raise an error if it's a formula - user should use the computed value
|
||||
if expr_info['value'] is None and expr_info['formula'] is not None:
|
||||
raise ValueError(
|
||||
f"Expression '{expression_name}' is a formula: {expr_info['formula']}. "
|
||||
f"This extractor requires a computed value, not a formula reference."
|
||||
)
|
||||
|
||||
return {
|
||||
expression_name: expr_info['value'],
|
||||
f'{expression_name}_units': expr_info['units']
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example usage
|
||||
import sys
|
||||
if len(sys.argv) > 2:
|
||||
prt_file = Path(sys.argv[1])
|
||||
expression_name = sys.argv[2]
|
||||
result = extract_expression(prt_file, expression_name)
|
||||
print(f"Extraction result: {result}")
|
||||
else:
|
||||
print(f"Usage: python {sys.argv[0]} <prt_file> <expression_name>")
|
||||
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Extract von Mises stress from solid and shell elements
|
||||
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
|
||||
|
||||
Pattern: element_stress
|
||||
Element Types: CTETRA, CHEXA, CPENTA (solids), CQUAD4, CTRIA3 (shells)
|
||||
Result Type: stress
|
||||
API: model.ctetra_stress[subcase], model.cquad4_stress[subcase], etc.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import numpy as np
|
||||
from pyNastran.op2.op2 import OP2
|
||||
|
||||
|
||||
def extract_solid_stress(op2_file: Path, subcase: int = 1, element_type: str = 'auto'):
|
||||
"""
|
||||
Extract stress from solid or shell elements.
|
||||
|
||||
Args:
|
||||
op2_file: Path to OP2 file
|
||||
subcase: Subcase number (default 1)
|
||||
element_type: Element type ('ctetra', 'chexa', 'cquad4', 'ctria3', or 'auto')
|
||||
'auto' will detect available element types
|
||||
|
||||
Returns:
|
||||
Dict with max von Mises stress and element info
|
||||
"""
|
||||
from pyNastran.op2.op2 import OP2
|
||||
import numpy as np
|
||||
|
||||
model = OP2()
|
||||
model.read_op2(str(op2_file))
|
||||
|
||||
# Auto-detect element type if requested
|
||||
if element_type == 'auto':
|
||||
# Try common element types in order
|
||||
# pyNastran uses "stress.{element}_stress" as attribute names (with dot in name!)
|
||||
possible_types = ['cquad4', 'ctria3', 'ctetra', 'chexa', 'cpenta']
|
||||
element_type = None
|
||||
|
||||
for elem_type in possible_types:
|
||||
stress_attr = f"stress.{elem_type}_stress"
|
||||
try:
|
||||
stress_dict = getattr(model, stress_attr, None)
|
||||
if isinstance(stress_dict, dict) and subcase in stress_dict:
|
||||
element_type = elem_type
|
||||
break
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
if element_type is None:
|
||||
raise ValueError(f"No stress results found in OP2 for subcase {subcase}")
|
||||
|
||||
# Get stress object for element type
|
||||
# pyNastran stores stress as "stress.{element}_stress" (e.g., "stress.cquad4_stress")
|
||||
stress_attr = f"stress.{element_type}_stress"
|
||||
|
||||
try:
|
||||
stress_dict = getattr(model, stress_attr)
|
||||
except AttributeError:
|
||||
raise ValueError(f"No {element_type} stress results in OP2")
|
||||
|
||||
if not isinstance(stress_dict, dict) or subcase not in stress_dict:
|
||||
raise ValueError(f"Subcase {subcase} not found in {element_type} stress results")
|
||||
|
||||
stress = stress_dict[subcase]
|
||||
itime = 0
|
||||
|
||||
# Extract von Mises
|
||||
# For CQUAD4/CTRIA3 (shells): data shape is [ntimes, nelements, 8]
|
||||
# Columns: [fiber_distance, oxx, oyy, txy, angle, omax, omin, von_mises]
|
||||
# Column 7 (0-indexed) is von Mises
|
||||
#
|
||||
# For CTETRA/CHEXA (solids): Column 9 is von Mises
|
||||
|
||||
if element_type in ['cquad4', 'ctria3']:
|
||||
# Shell elements - von Mises is at column 7
|
||||
von_mises = stress.data[itime, :, 7]
|
||||
else:
|
||||
# Solid elements - von Mises is at column 9
|
||||
von_mises = stress.data[itime, :, 9]
|
||||
|
||||
# Raw stress values from OP2 (in internal Nastran units)
|
||||
max_stress_raw = float(np.max(von_mises))
|
||||
min_stress_raw = float(np.min(von_mises))
|
||||
avg_stress_raw = float(np.mean(von_mises))
|
||||
|
||||
# Convert to MPa
|
||||
# For MN-MM unit system (UNITSYS=MN-MM), Nastran outputs stress with implied decimal
|
||||
# The raw value needs to be divided by 1000 to get MPa
|
||||
# Example: 131507.1875 (raw) = 131.507 MPa
|
||||
max_stress_mpa = max_stress_raw / 1000.0
|
||||
min_stress_mpa = min_stress_raw / 1000.0
|
||||
avg_stress_mpa = avg_stress_raw / 1000.0
|
||||
|
||||
# Get element info
|
||||
if hasattr(stress, 'element_node'):
|
||||
element_ids = stress.element_node[:, 0] # First column is element ID
|
||||
elif hasattr(stress, 'element'):
|
||||
element_ids = stress.element
|
||||
else:
|
||||
element_ids = np.arange(len(von_mises))
|
||||
|
||||
max_stress_elem = int(element_ids[np.argmax(von_mises)])
|
||||
|
||||
return {
|
||||
'max_von_mises': max_stress_mpa,
|
||||
'min_von_mises': min_stress_mpa,
|
||||
'avg_von_mises': avg_stress_mpa,
|
||||
'max_stress_element': max_stress_elem,
|
||||
'element_type': element_type,
|
||||
'num_elements': len(von_mises),
|
||||
'units': 'MPa'
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example usage
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
op2_file = Path(sys.argv[1])
|
||||
result = extract_solid_stress(op2_file)
|
||||
print(f"Extraction result: {result}")
|
||||
else:
|
||||
print("Usage: python {sys.argv[0]} <op2_file>")
|
||||
264
archive/deprecated/result_extractors/op2_extractor_example.py
Normal file
264
archive/deprecated/result_extractors/op2_extractor_example.py
Normal file
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
Example: Result Extraction from OP2 files using pyNastran
|
||||
|
||||
This shows how to extract optimization metrics from Nastran OP2 files.
|
||||
Common metrics:
|
||||
- Max displacement (for stiffness constraints)
|
||||
- Max von Mises stress (for strength constraints)
|
||||
- Mass (for minimization objectives)
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import numpy as np
|
||||
|
||||
|
||||
def extract_max_displacement(op2_path: Path) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract maximum displacement magnitude from OP2 file.
|
||||
|
||||
Args:
|
||||
op2_path: Path to .op2 file
|
||||
|
||||
Returns:
|
||||
Dictionary with max displacement, node ID, and components
|
||||
"""
|
||||
from pyNastran.op2.op2 import OP2
|
||||
|
||||
op2 = OP2()
|
||||
op2.read_op2(str(op2_path))
|
||||
|
||||
# Get first subcase (usually the only one in static analysis)
|
||||
subcase_id = list(op2.displacements.keys())[0]
|
||||
displacements = op2.displacements[subcase_id]
|
||||
|
||||
# Extract node IDs and displacement data
|
||||
node_ids = displacements.node_gridtype[:, 0].astype(int)
|
||||
disp_data = displacements.data[0] # First (and usually only) timestep
|
||||
|
||||
# Calculate magnitude: sqrt(dx^2 + dy^2 + dz^2)
|
||||
dx = disp_data[:, 0]
|
||||
dy = disp_data[:, 1]
|
||||
dz = disp_data[:, 2]
|
||||
magnitudes = np.sqrt(dx**2 + dy**2 + dz**2)
|
||||
|
||||
# Find max
|
||||
max_idx = np.argmax(magnitudes)
|
||||
max_displacement = magnitudes[max_idx]
|
||||
max_node_id = node_ids[max_idx]
|
||||
|
||||
return {
|
||||
'max_displacement': float(max_displacement),
|
||||
'max_node_id': int(max_node_id),
|
||||
'dx': float(dx[max_idx]),
|
||||
'dy': float(dy[max_idx]),
|
||||
'dz': float(dz[max_idx]),
|
||||
'units': 'mm', # NX typically uses mm
|
||||
'subcase': subcase_id
|
||||
}
|
||||
|
||||
|
||||
def extract_max_stress(op2_path: Path, stress_type: str = 'von_mises') -> Dict[str, Any]:
|
||||
"""
|
||||
Extract maximum stress from OP2 file.
|
||||
|
||||
Args:
|
||||
op2_path: Path to .op2 file
|
||||
stress_type: 'von_mises' or 'max_principal'
|
||||
|
||||
Returns:
|
||||
Dictionary with max stress, element ID, and location
|
||||
"""
|
||||
from pyNastran.op2.op2 import OP2
|
||||
|
||||
op2 = OP2(debug=False)
|
||||
op2.read_op2(str(op2_path))
|
||||
|
||||
# Stress can be in different tables depending on element type
|
||||
# Common: cquad4_stress, ctria3_stress, ctetra_stress, etc.
|
||||
stress_tables = [
|
||||
'cquad4_stress',
|
||||
'ctria3_stress',
|
||||
'ctetra_stress',
|
||||
'chexa_stress',
|
||||
'cpenta_stress',
|
||||
'cbar_stress',
|
||||
'cbeam_stress'
|
||||
]
|
||||
|
||||
max_stress_overall = 0.0
|
||||
max_element_id = None
|
||||
max_element_type = None
|
||||
|
||||
# Try to get stress from different pyNastran API formats
|
||||
for table_name in stress_tables:
|
||||
stress_table = None
|
||||
|
||||
# Try format 1: Attribute name with dot (e.g., 'stress.chexa_stress')
|
||||
# This is used in newer pyNastran versions
|
||||
dotted_name = f'stress.{table_name}'
|
||||
if hasattr(op2, dotted_name):
|
||||
stress_table = getattr(op2, dotted_name)
|
||||
# Try format 2: Nested attribute op2.stress.chexa_stress
|
||||
elif hasattr(op2, 'stress') and hasattr(op2.stress, table_name):
|
||||
stress_table = getattr(op2.stress, table_name)
|
||||
# Try format 3: Direct attribute op2.chexa_stress (older pyNastran)
|
||||
elif hasattr(op2, table_name):
|
||||
stress_table = getattr(op2, table_name)
|
||||
|
||||
if stress_table:
|
||||
subcase_id = list(stress_table.keys())[0]
|
||||
stress_data = stress_table[subcase_id]
|
||||
|
||||
# Extract von Mises stress
|
||||
# Note: Structure varies by element type
|
||||
element_ids = stress_data.element_node[:, 0].astype(int)
|
||||
|
||||
if stress_type == 'von_mises':
|
||||
# For solid elements (CHEXA, CTETRA, CPENTA): von Mises is at index 9
|
||||
# For shell elements (CQUAD4, CTRIA3): von Mises is last column (-1)
|
||||
if table_name in ['chexa_stress', 'ctetra_stress', 'cpenta_stress']:
|
||||
# Solid elements: data shape is [itime, nnodes, 10]
|
||||
# Index 9 is von_mises [oxx, oyy, ozz, txy, tyz, txz, o1, o2, o3, von_mises]
|
||||
stresses = stress_data.data[0, :, 9]
|
||||
else:
|
||||
# Shell elements: von Mises is last column
|
||||
stresses = stress_data.data[0, :, -1]
|
||||
else:
|
||||
# Max principal stress
|
||||
if table_name in ['chexa_stress', 'ctetra_stress', 'cpenta_stress']:
|
||||
stresses = stress_data.data[0, :, 6] # o1 (max principal)
|
||||
else:
|
||||
stresses = stress_data.data[0, :, -2]
|
||||
|
||||
max_stress_in_table = np.max(stresses)
|
||||
if max_stress_in_table > max_stress_overall:
|
||||
max_stress_overall = max_stress_in_table
|
||||
max_idx = np.argmax(stresses)
|
||||
max_element_id = element_ids[max_idx]
|
||||
max_element_type = table_name.replace('_stress', '')
|
||||
|
||||
# CRITICAL: NX Nastran outputs stress in kPa (mN/mm²), convert to MPa
|
||||
# 1 kPa = 0.001 MPa
|
||||
max_stress_overall_mpa = max_stress_overall / 1000.0
|
||||
|
||||
return {
|
||||
'max_stress': float(max_stress_overall_mpa),
|
||||
'stress_type': stress_type,
|
||||
'element_id': int(max_element_id) if max_element_id else None,
|
||||
'element_type': max_element_type,
|
||||
'units': 'MPa',
|
||||
}
|
||||
|
||||
|
||||
def extract_mass(op2_path: Path) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract total mass from OP2 file.
|
||||
|
||||
Args:
|
||||
op2_path: Path to .op2 file
|
||||
|
||||
Returns:
|
||||
Dictionary with mass and center of gravity
|
||||
"""
|
||||
from pyNastran.op2.op2 import OP2
|
||||
|
||||
op2 = OP2()
|
||||
op2.read_op2(str(op2_path))
|
||||
|
||||
# Mass is in grid_point_weight table
|
||||
if hasattr(op2, 'grid_point_weight') and op2.grid_point_weight:
|
||||
mass_data = op2.grid_point_weight
|
||||
|
||||
# Total mass
|
||||
total_mass = mass_data.mass.sum()
|
||||
|
||||
# Center of gravity
|
||||
cg = mass_data.cg
|
||||
|
||||
return {
|
||||
'total_mass': float(total_mass),
|
||||
'cg_x': float(cg[0]),
|
||||
'cg_y': float(cg[1]),
|
||||
'cg_z': float(cg[2]),
|
||||
'units': 'kg'
|
||||
}
|
||||
else:
|
||||
# Fallback: Mass not directly available
|
||||
return {
|
||||
'total_mass': None,
|
||||
'note': 'Mass data not found in OP2 file. Ensure PARAM,GRDPNT,0 is in Nastran deck'
|
||||
}
|
||||
|
||||
|
||||
# Combined extraction function for optimization
|
||||
def extract_all_results(op2_path: Path) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract all common optimization metrics from OP2 file.
|
||||
|
||||
Args:
|
||||
op2_path: Path to .op2 file
|
||||
|
||||
Returns:
|
||||
Dictionary with all results
|
||||
"""
|
||||
results = {
|
||||
'op2_file': str(op2_path),
|
||||
'status': 'success'
|
||||
}
|
||||
|
||||
try:
|
||||
results['displacement'] = extract_max_displacement(op2_path)
|
||||
except Exception as e:
|
||||
results['displacement'] = {'error': str(e)}
|
||||
|
||||
try:
|
||||
results['stress'] = extract_max_stress(op2_path)
|
||||
except Exception as e:
|
||||
results['stress'] = {'error': str(e)}
|
||||
|
||||
try:
|
||||
results['mass'] = extract_mass(op2_path)
|
||||
except Exception as e:
|
||||
results['mass'] = {'error': str(e)}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import json
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python op2_extractor_example.py <path_to_op2_file>")
|
||||
sys.exit(1)
|
||||
|
||||
op2_path = Path(sys.argv[1])
|
||||
|
||||
if not op2_path.exists():
|
||||
print(f"Error: File not found: {op2_path}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Extracting results from: {op2_path}")
|
||||
print("=" * 60)
|
||||
|
||||
results = extract_all_results(op2_path)
|
||||
|
||||
print("\nResults:")
|
||||
print(json.dumps(results, indent=2))
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("SUMMARY:")
|
||||
if 'displacement' in results and 'max_displacement' in results['displacement']:
|
||||
disp = results['displacement']
|
||||
print(f" Max Displacement: {disp['max_displacement']:.6f} {disp['units']} at node {disp['max_node_id']}")
|
||||
|
||||
if 'stress' in results and 'max_stress' in results['stress']:
|
||||
stress = results['stress']
|
||||
print(f" Max {stress['stress_type']}: {stress['max_stress']:.2f} {stress['units']} in element {stress['element_id']}")
|
||||
|
||||
if 'mass' in results and 'total_mass' in results['mass'] and results['mass']['total_mass']:
|
||||
mass = results['mass']
|
||||
print(f" Total Mass: {mass['total_mass']:.6f} {mass['units']}")
|
||||
Reference in New Issue
Block a user