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:
@@ -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>")
|
||||
Reference in New Issue
Block a user