feat: Add Studio UI, intake system, and extractor improvements

Dashboard:
- Add Studio page with drag-drop model upload and Claude chat
- Add intake system for study creation workflow
- Improve session manager and context builder
- Add intake API routes and frontend components

Optimization Engine:
- Add CLI module for command-line operations
- Add intake module for study preprocessing
- Add validation module with gate checks
- Improve Zernike extractor documentation
- Update spec models with better validation
- Enhance solve_simulation robustness

Documentation:
- Add ATOMIZER_STUDIO.md planning doc
- Add ATOMIZER_UX_SYSTEM.md for UX patterns
- Update extractor library docs
- Add study-readme-generator skill

Tools:
- Add test scripts for extraction validation
- Add Zernike recentering test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 12:02:30 -05:00
parent 3193831340
commit a26914bbe8
56 changed files with 14173 additions and 646 deletions

View File

@@ -1,74 +1,86 @@
"""
Extract maximum von Mises stress from structural analysis
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Extract maximum von Mises stress from structural analysis.
Pattern: solid_stress
Element Type: CTETRA
Result Type: stress
API: model.ctetra_stress[subcase] or model.chexa_stress[subcase]
Supports all solid element types (CTETRA, CHEXA, CPENTA, CPYRAM) and
shell elements (CQUAD4, CTRIA3).
Unit Note: NX Nastran in kg-mm-s outputs stress in kPa. This extractor
converts to MPa (divide by 1000) for engineering use.
"""
from pathlib import Path
from typing import Dict, Any
from typing import Dict, Any, Optional
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_solid_stress(op2_file: Path, subcase: int = 1, element_type: str = 'ctetra'):
"""Extract stress from solid elements."""
from pyNastran.op2.op2 import OP2
import numpy as np
def extract_solid_stress(
op2_file: Path,
subcase: int = 1,
element_type: Optional[str] = None,
convert_to_mpa: bool = True,
) -> Dict[str, Any]:
"""
Extract maximum von Mises stress from solid elements.
model = OP2()
Args:
op2_file: Path to OP2 results file
subcase: Subcase ID (default 1)
element_type: Specific element type to check ('ctetra', 'chexa', etc.)
If None, checks ALL solid element types and returns max.
convert_to_mpa: If True, divide by 1000 to convert kPa to MPa (default True)
Returns:
dict with 'max_von_mises' (in MPa if convert_to_mpa=True),
'max_stress_element', and 'element_type'
"""
model = OP2(debug=False, log=None)
model.read_op2(str(op2_file))
# Get stress object for element type
# Different element types have different stress attributes
stress_attr_map = {
'ctetra': 'ctetra_stress',
'chexa': 'chexa_stress',
'cquad4': 'cquad4_stress',
'ctria3': 'ctria3_stress'
}
# All solid element types to check
solid_element_types = ["ctetra", "chexa", "cpenta", "cpyram"]
shell_element_types = ["cquad4", "ctria3"]
stress_attr = stress_attr_map.get(element_type.lower())
if not stress_attr:
raise ValueError(f"Unknown element type: {element_type}")
# Access stress through op2_results container
# pyNastran structure: model.op2_results.stress.cquad4_stress[subcase]
stress_dict = None
if hasattr(model, 'op2_results') and hasattr(model.op2_results, 'stress'):
stress_container = model.op2_results.stress
if hasattr(stress_container, stress_attr):
stress_dict = getattr(stress_container, stress_attr)
if stress_dict is None:
raise ValueError(f"No {element_type} stress results in OP2. Available attributes: {[a for a in dir(model) if 'stress' in a.lower()]}")
# stress_dict is a dictionary with subcase IDs as keys
available_subcases = list(stress_dict.keys())
if not available_subcases:
raise ValueError(f"No stress data found in OP2 file")
# Use the specified subcase or first available
if subcase in available_subcases:
actual_subcase = subcase
# If specific element type requested, only check that one
if element_type:
element_types_to_check = [element_type.lower()]
else:
actual_subcase = available_subcases[0]
# Check all solid types by default
element_types_to_check = solid_element_types
stress = stress_dict[actual_subcase]
if not hasattr(model, "op2_results") or not hasattr(model.op2_results, "stress"):
raise ValueError("No stress results in OP2 file")
itime = 0
stress_container = model.op2_results.stress
# Extract von Mises if available
if stress.is_von_mises: # Property, not method
# Different element types have von Mises at different column indices
# Shell elements (CQUAD4, CTRIA3): 8 columns, von Mises at column 7
# Solid elements (CTETRA, CHEXA): 10 columns, von Mises at column 9
# Find max stress across all requested element types
max_stress = 0.0
max_stress_elem = 0
max_stress_type = None
for elem_type in element_types_to_check:
stress_attr = f"{elem_type}_stress"
if not hasattr(stress_container, stress_attr):
continue
stress_dict = getattr(stress_container, stress_attr)
if not stress_dict:
continue
# Get subcase
available_subcases = list(stress_dict.keys())
if not available_subcases:
continue
actual_subcase = subcase if subcase in available_subcases else available_subcases[0]
stress = stress_dict[actual_subcase]
if not stress.is_von_mises:
continue
# Determine von Mises column
ncols = stress.data.shape[2]
if ncols == 8:
# Shell elements - von Mises is last column
von_mises_col = 7
@@ -76,27 +88,37 @@ def extract_solid_stress(op2_file: Path, subcase: int = 1, element_type: str = '
# Solid elements - von Mises is column 9
von_mises_col = 9
else:
# Unknown format, try last column
von_mises_col = ncols - 1
itime = 0
von_mises = stress.data[itime, :, von_mises_col]
max_stress = float(np.max(von_mises))
elem_max = float(np.max(von_mises))
# Get element info
element_ids = [eid for (eid, node) in stress.element_node]
max_stress_elem = element_ids[np.argmax(von_mises)]
if elem_max > max_stress:
max_stress = elem_max
element_ids = [eid for (eid, node) in stress.element_node]
max_stress_elem = int(element_ids[np.argmax(von_mises)])
max_stress_type = elem_type.upper()
return {
'max_von_mises': max_stress,
'max_stress_element': int(max_stress_elem)
}
else:
raise ValueError("von Mises stress not available")
if max_stress_type is None:
raise ValueError(f"No stress results found for element types: {element_types_to_check}")
# Convert from kPa to MPa (NX kg-mm-s unit system outputs kPa)
if convert_to_mpa:
max_stress = max_stress / 1000.0
return {
"max_von_mises": max_stress,
"max_stress_element": max_stress_elem,
"element_type": max_stress_type,
"units": "MPa" if convert_to_mpa else "kPa",
}
if __name__ == '__main__':
if __name__ == "__main__":
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_solid_stress(op2_file)