Files
Atomizer/studies/bracket_stiffness_optimization/bracket_stiffness_extractor.py
Anto01 a4805947d1 feat: Add NX study models and optimization histories
Includes all study folders with NX models for development:
- bracket_stiffness_optimization (V1, V2, V3)
- drone_gimbal_arm_optimization
- simple_beam_optimization
- uav_arm_optimization (V1, V2)
- training_data_export_test
- uav_arm_atomizerfield_test

Contains .prt, .fem, .sim files and optimization databases.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 12:19:07 -05:00

243 lines
8.9 KiB
Python

"""
Bracket Stiffness Optimization - Results Extractor
==================================================
This extractor uses the generic tools from optimization_engine/extractors/ to:
1. Execute NX journal to export displacement field
2. Extract z-displacement from field data
3. Extract applied force from OP2 file
4. Calculate stiffness (k = F/δ)
5. Extract mass from OP2 file
This is a thin wrapper around generic extractors - all the heavy lifting
is done by reusable components.
"""
import sys
from pathlib import Path
from typing import Dict, Any, Tuple
# Add project root to path
project_root = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(project_root))
from optimization_engine.extractors.stiffness_calculator import StiffnessCalculator
from optimization_engine.extractors.bdf_mass_extractor import BDFMassExtractor
from optimization_engine.nx_solver import NXSolver
# Import central configuration
import config as atomizer_config
class BracketStiffnessExtractor:
"""
Bracket-specific extractor that orchestrates generic tools.
Extracts:
- Stiffness (N/mm) from force and z-displacement
- Mass (kg) from OP2 grid point weight
"""
def __init__(
self,
model_dir: Path,
sim_file: str = "Bracket_sim1.sim",
export_journal: str = "export_displacement_field.py",
):
"""
Args:
model_dir: Directory containing model files
sim_file: Name of .sim file
export_journal: Name of journal that exports displacement field
"""
self.model_dir = Path(model_dir)
self.sim_file = self.model_dir / sim_file
self.export_journal = self.model_dir / export_journal
self.sim_base = Path(sim_file).stem # e.g., "Bracket_sim1" -> "Bracket_sim1"
# Expected output files from NX
self.field_file = self.model_dir / "export_field_dz.fld"
# NX creates OP2 with lowercase base name and solution suffix
self.op2_file = self.model_dir / f"{self.sim_base.lower()}-solution_1.op2"
# BDF/DAT file for mass extraction
self.dat_file = self.model_dir / f"{self.sim_base.lower()}-solution_1.dat"
def extract_results(self) -> Dict[str, Any]:
"""
Extract stiffness and mass from FEA results.
Returns:
dict: {
'stiffness': stiffness value (N/mm),
'mass': mass in kg,
'mass_g': mass in grams,
'displacement': max z-displacement (mm),
'force': applied force (N),
'compliance': inverse stiffness (mm/N),
'objectives': {
'stiffness': value for maximization,
'mass': value for constraint checking
}
}
"""
# Step 1: Execute NX journal to export displacement field
print(f"Executing journal to export displacement field...")
self._export_displacement_field()
# Verify field file was created
if not self.field_file.exists():
raise FileNotFoundError(f"Field file not created: {self.field_file}")
# Verify OP2 file exists
if not self.op2_file.exists():
raise FileNotFoundError(f"OP2 file not found: {self.op2_file}")
# Step 2: Calculate stiffness using generic calculator
print(f"Calculating stiffness...")
stiffness_calc = StiffnessCalculator(
field_file=str(self.field_file),
op2_file=str(self.op2_file),
force_component="fz", # Z-direction force
displacement_component="z", # Z-displacement
displacement_aggregation="max_abs", # Maximum absolute displacement
applied_force=1000.0 # Applied load is 1000N (constant for this model)
)
stiffness_results = stiffness_calc.calculate()
# Step 3: Extract mass from BDF/DAT file
print(f"Extracting mass from BDF...")
if not self.dat_file.exists():
raise FileNotFoundError(f"DAT file not found: {self.dat_file}")
bdf_extractor = BDFMassExtractor(bdf_file=str(self.dat_file))
mass_results = bdf_extractor.extract_mass()
# Step 4: Combine results
results = {
'stiffness': stiffness_results['stiffness'],
'mass': mass_results['mass_kg'],
'mass_g': mass_results['mass_g'],
'displacement': stiffness_results['displacement'],
'force': stiffness_results['force'],
'compliance': stiffness_results['compliance'],
'objectives': {
'stiffness': stiffness_results['stiffness'], # Maximize
'mass': mass_results['mass_kg'] # Constrain ≤ 0.2 kg
},
'details': {
'stiffness_stats': stiffness_results['displacement_stats'],
'mass_cg': mass_results.get('cg'),
'units': stiffness_results['units']
}
}
print(f"\n[OK] Stiffness: {results['stiffness']:.2f} N/mm")
print(f"[OK] Mass: {results['mass']:.6f} kg ({results['mass_g']:.2f} g)")
print(f"[OK] Displacement: {results['displacement']:.6f} mm")
print(f"[OK] Force: {results['force']:.2f} N")
return results
def _export_displacement_field(self):
"""
Execute NX journal to export displacement field.
The journal should:
1. Open the simulation
2. Export ResultProbe to field file (.fld)
3. Save and close
"""
if not self.export_journal.exists():
raise FileNotFoundError(f"Export journal not found: {self.export_journal}")
# Use NXSolver to execute journal
# Note: This assumes NXSolver can run journals in non-solve mode
# If not, we'll need to create a separate journal runner
try:
from optimization_engine.nx_solver import run_journal
run_journal(str(self.export_journal))
except ImportError:
# Fallback: Execute journal directly via NX command line
import subprocess
nx_exe = atomizer_config.NX_RUN_JOURNAL
if Path(nx_exe).exists():
# Note: NX's run_journal.exe may return non-zero even on success due to sys.exit() handling
# We check for field file existence instead of return code
result = subprocess.run([nx_exe, str(self.export_journal)], capture_output=True, text=True)
# If field file doesn't exist after running journal, something went wrong
if not self.field_file.exists():
raise RuntimeError(
f"Journal execution completed but field file not created: {self.field_file}\n"
f"Journal output:\n{result.stdout}\n{result.stderr}"
)
else:
raise RuntimeError(
f"Cannot execute journal. NX executable not found: {nx_exe}\n"
f"Please execute journal manually: {self.export_journal}"
)
def extract_bracket_stiffness(
model_dir: str,
sim_file: str = "Bracket_sim1.sim"
) -> Tuple[float, float]:
"""
Convenience function to extract stiffness and mass.
Args:
model_dir: Directory containing bracket model files
sim_file: Name of simulation file
Returns:
(stiffness, mass): Stiffness in N/mm, mass in kg
"""
extractor = BracketStiffnessExtractor(
model_dir=Path(model_dir),
sim_file=sim_file
)
results = extractor.extract_results()
return results['stiffness'], results['mass']
if __name__ == "__main__":
# Example usage / testing
import sys
if len(sys.argv) > 1:
model_dir = sys.argv[1]
else:
# Default to study model directory
model_dir = Path(__file__).parent / "1_setup" / "model"
print(f"Testing bracket stiffness extractor...")
print(f"Model directory: {model_dir}\n")
extractor = BracketStiffnessExtractor(model_dir=model_dir)
try:
results = extractor.extract_results()
print("\n" + "="*60)
print("EXTRACTION RESULTS")
print("="*60)
print(f"Stiffness: {results['stiffness']:.2f} N/mm")
print(f"Mass: {results['mass']:.6f} kg ({results['mass_g']:.2f} g)")
print(f"Displacement: {results['displacement']:.6f} mm")
print(f"Force: {results['force']:.2f} N")
print(f"Compliance: {results['compliance']:.6e} mm/N")
print("="*60)
# Check constraint
max_mass_kg = 0.2
if results['mass'] <= max_mass_kg:
print(f"[OK] Mass constraint satisfied: {results['mass']:.6f} kg <= {max_mass_kg} kg")
else:
print(f"[X] Mass constraint violated: {results['mass']:.6f} kg > {max_mass_kg} kg")
except Exception as e:
print(f"\n[X] Extraction failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)