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:
Antoine
2025-12-12 11:24:02 -05:00
parent 1bb201e0b7
commit d1261d62fd
58 changed files with 26 additions and 10731 deletions

View File

@@ -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>")

View 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>")

View 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>")

View File

@@ -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>")