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>
243 lines
8.9 KiB
Python
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)
|