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>
This commit is contained in:
2025-11-26 12:19:07 -05:00
parent 2b3573ec42
commit a4805947d1
166 changed files with 11515 additions and 1 deletions

View File

@@ -0,0 +1,91 @@
"""
NX Journal - Export Displacement Field for Bracket Stiffness Analysis
=====================================================================
This journal exports the z-displacement field from a ResultProbe to a .fld file.
Usage:
run_journal.exe export_displacement_field.py [sim_file_path]
If sim_file_path is not provided, uses Bracket_sim1.sim in the same directory.
"""
import sys
import math
from pathlib import Path
import NXOpen
import NXOpen.CAE
import NXOpen.Fields
def main(args):
"""
Export displacement field from NX simulation results.
Args:
args: Command line arguments, optionally including sim file path
The ResultProbe should already be defined in the simulation file
with z-displacement as the measured quantity.
"""
theSession = NXOpen.Session.GetSession()
# Determine sim file to open
if len(args) > 0:
sim_file = Path(args[0])
else:
# Default: Bracket_sim1.sim in same directory as this journal
journal_dir = Path(__file__).parent
sim_file = journal_dir / "Bracket_sim1.sim"
if not sim_file.exists():
print(f"ERROR: Simulation file not found: {sim_file}")
return 1
# Open the simulation file
print(f"Opening simulation: {sim_file}")
try:
basePart1, partLoadStatus1 = theSession.Parts.OpenBaseDisplay(str(sim_file))
partLoadStatus1.Dispose()
except Exception as e:
print(f"ERROR: Failed to open simulation: {e}")
return 1
workSimPart = theSession.Parts.BaseWork
if workSimPart is None:
print("ERROR: No work part loaded after opening simulation.")
return 1
# Get the FieldManager
fieldManager = workSimPart.FindObject("FieldManager")
if fieldManager is None:
print("ERROR: FieldManager not found. Make sure simulation results are loaded.")
return 1
# Find the ResultProbe (should be pre-configured for z-displacement)
resultProbe = fieldManager.FindObject("ResultProbe")
if resultProbe is None:
print("ERROR: ResultProbe not found. Please create a ResultProbe for z-displacement.")
return 1
# Prepare probe array for export
probes = [NXOpen.CAE.ResultProbe.Null] * 1
probes[0] = resultProbe
# Determine output file path (same directory as this journal)
journal_dir = Path(__file__).parent
output_file = journal_dir / "export_field_dz.fld"
# Export to field file
print(f"Exporting displacement field to: {output_file}")
theSession.ResultManager.ExportProbesToFieldFile(probes, str(output_file))
print(f"[OK] Successfully exported displacement field")
print(f" Output: {output_file}")
return 0
if __name__ == '__main__':
exit_code = main(sys.argv[1:])
sys.exit(exit_code)

View File

@@ -0,0 +1,72 @@
FIELD: [ResultProbe] : [TABLE]
FIELD LOCK STATE: [NO]
DUPLICATE_VALUE_OPTION: [0]
PARAMETERIZE INDEPENDENT DOMAIN: [NO]
PERSIST INTERPOL: [NO]
CREATE INTERPOLATOR: [NO]
FALLBACK DEFAULT INTERPOLATOR: [YES]
INTERPOL: [4]
VALUES OUTSIDE: [2]
REMOVE DELAUNAY SLIVERS: [NO]
INDEP VAR: [step] : [] : [] : [0]
BOUNDS: [0] : [YES] : [0] : [YES] : [51] : [0]
INDEP VAR: [node_id] : [] : [] : [5]
BOUNDS: [3] : [YES] : [669] : [YES] : [51] : [655]
DEP VAR: [x] : [Length] : [mm] : [0]
DESCRIPTION: ResultProbe
DESCRIPTION: dz
DESCRIPTION: 21-Nov-25
DESCRIPTION: 15:27:24
START DATA
0, 655, -0.0374058224260807
0, 656, -0.038049004971981
0, 657, -0.0369194410741329
0, 658, -0.0365256853401661
0, 659, -0.0362277515232563
0, 660, -0.0421693958342075
0, 661, -0.038899153470993
0, 662, -0.040073998272419
0, 663, -0.0356613844633102
0, 664, -0.0356574058532715
0, 665, -0.0358434692025185
0, 666, -0.0357435494661331
0, 667, -0.0360030345618725
0, 668, -0.0356605499982834
0, 669, -0.0356847979128361
0, 3, -0.0459242127835751
0, 6, -0.0459244512021542
0, 14, -0.0356661044061184
0, 11, -0.0356660187244415
0, 128, -0.0374444201588631
0, 129, -0.0381020344793797
0, 130, -0.0369499586522579
0, 131, -0.0365508943796158
0, 132, -0.0362492613494396
0, 133, -0.0389753989875317
0, 134, -0.040153156965971
0, 135, -0.0421142838895321
0, 136, -0.0356643460690975
0, 137, -0.0356617495417595
0, 138, -0.0356673523783684
0, 139, -0.0360213443636894
0, 140, -0.0358589664101601
0, 141, -0.0357562974095345
0, 142, -0.0356945171952248
0, 59, -0.0461513064801693
0, 169, -0.0374445840716362
0, 170, -0.0381022356450558
0, 171, -0.0369501039385796
0, 172, -0.0365510284900665
0, 173, -0.0362493842840195
0, 174, -0.0421144664287567
0, 175, -0.040153443813324
0, 176, -0.038975652307272
0, 177, -0.0356644317507744
0, 178, -0.0356618389487267
0, 179, -0.0358590744435787
0, 180, -0.0357564017176628
0, 181, -0.0360214598476887
0, 182, -0.0356946140527725
0, 183, -0.0356674455106258
0, 145, -0.0356633476912975
END DATA

View File

@@ -0,0 +1,12 @@
{
"timestamp": "2025-11-21T15:27:28.828930",
"trial_number": 29,
"total_trials": 30,
"current_phase": "characterization",
"current_strategy": null,
"is_multi_objective": true,
"study_directions": [
"1",
"1"
]
}

View File

@@ -0,0 +1,45 @@
{
"study_name": "bracket_stiffness_optimization_V2",
"optimizer": "Protocol 10 - Intelligent Multi-Strategy",
"n_trials": 30,
"completed_trials": 30,
"feasible_trials": 30,
"infeasible_trials": 0,
"pruned_trials": 0,
"failed_trials": 0,
"elapsed_seconds": 434.682016,
"pareto_front_all": 20,
"pareto_front_feasible": 20,
"best_solution": {
"best_params": {
"support_angle": 51.35737604370232,
"tip_thickness": 32.57376564160467
},
"best_value": [
-10570.444122105115,
0.10978219259872178
],
"best_trial": 1,
"is_multi_objective": true,
"pareto_front_size": 20,
"total_trials": 30,
"final_strategy": "random",
"landscape_analysis": {
"ready": false,
"total_trials": 30,
"message": "Landscape analysis not supported for multi-objective optimization"
},
"strategy_recommendation": {
"confidence": 1.0,
"reasoning": "Insufficient data (30 trials) - using random exploration for landscape characterization",
"sampler_config": {
"type": "RandomSampler",
"params": {}
}
},
"transition_history": [],
"strategy_performance": {},
"protocol_used": "Protocol 10: Intelligent Multi-Strategy Optimization"
},
"timestamp": "2025-11-21T15:27:28.910034"
}

View File

@@ -0,0 +1,256 @@
# Bracket Stiffness Optimization Study
Multi-objective optimization to maximize bracket stiffness while minimizing mass.
## Objectives
1. **Maximize Stiffness** (Primary)
- Structural stiffness calculated as k = F/δ
- Units: N/mm
- Higher is better
2. **Minimize Mass** (Secondary + Constraint)
- Total bracket mass
- Units: kg
- **Hard Constraint**: Mass ≤ 0.2 kg (200 grams)
## Design Variables
| Variable | Min | Max | Initial | Unit | Description |
|----------|-----|-----|---------|------|-------------|
| `support_angle` | 30.0 | 90.0 | 60.0 | degrees | Angle of support arm relative to base |
| `tip_thickness` | 15.0 | 40.0 | 25.0 | mm | Thickness of bracket tip where load is applied |
## Study Structure
```
bracket_stiffness_optimization/
├── README.md # This file
├── optimization_config.json # Optimization settings
├── workflow_config.json # Workflow definition
├── run_optimization.py # Main runner script
├── bracket_stiffness_extractor.py # Results extractor
├── 1_setup/
│ └── model/
│ ├── Bracket.prt # NX part file
│ ├── Bracket_fem1_i.prt # Idealized geometry
│ ├── Bracket_fem1.fem # FEM definition
│ ├── Bracket_sim1.sim # Simulation file
│ └── export_displacement_field.py # NX journal for field export
└── 2_results/
├── study.db # Optuna study database
├── optimization_summary.json # Results summary
├── workflow.log # Execution log
└── errors.log # Error log
```
## Generic Extractors Used
This study uses **reusable generic extractors** from `optimization_engine/extractors/`:
1. **`field_data_extractor.py`**
- Parses NX exported field data (.fld files)
- Works for: displacement, stress, strain, temperature, any scalar field
- Multiple aggregation methods: max_abs, max, min, mean, std
2. **`op2_extractor.py`**
- Extracts data from Nastran OP2 files using pyNastran
- Mass properties (with unit conversion ton→kg)
- Grid point forces (fx, fy, fz, resultant)
- ~10000x faster than F06 parsing
3. **`stiffness_calculator.py`**
- Generic stiffness calculator: k = F/δ
- Works for any structure: bracket, beam, plate, etc.
- Combines field data and OP2 extractors
The `bracket_stiffness_extractor.py` is a thin wrapper that orchestrates these generic tools with bracket-specific parameters.
## Workflow
Each optimization trial follows these steps:
1. **Update Design Variables**
- Modify NX model expressions: `support_angle`, `tip_thickness`
2. **Solve Simulation**
- Run NX Nastran SOL 101 (linear static analysis)
- Output: `.op2` and `.f06` files
3. **Export Displacement Field**
- Execute NX journal to export z-displacement
- Output: `export_field_dz.fld`
4. **Extract Results**
- Parse displacement field (max absolute z-displacement)
- Extract applied force from OP2
- Calculate stiffness: k = Force / Displacement
- Extract mass from OP2 grid point weight
5. **Evaluate Constraints**
- Check: mass ≤ 0.2 kg
- If violated: prune trial
6. **Report Results**
- Send to Optuna study database
- Broadcast to dashboard via WebSocket
## Running the Optimization
### Basic Usage
```bash
cd studies/bracket_stiffness_optimization
python run_optimization.py
```
### Advanced Options
```bash
# Custom number of trials
python run_optimization.py --trials 100
# Enable real-time dashboard
python run_optimization.py --dashboard
# Both
python run_optimization.py --trials 50 --dashboard
```
### Testing Before Full Run
Test the extractors with a single trial first:
```bash
# Test extractor independently
python bracket_stiffness_extractor.py
```
## Results Analysis
### View in Dashboard
The React dashboard provides real-time monitoring:
- Convergence plot (stiffness over trials)
- Parameter space exploration
- Pareto front visualization
- Mass constraint violations
Access at: `http://localhost:3001`
### Database Query
Results are stored in SQLite database `2_results/study.db`:
```python
import optuna
study = optuna.load_study(
study_name="bracket_stiffness_optimization",
storage="sqlite:///2_results/study.db"
)
# Get Pareto-optimal solutions
best_trials = study.best_trials
for trial in best_trials:
stiffness_neg, mass = trial.values
stiffness = -stiffness_neg
print(f"Trial {trial.number}: Stiffness={stiffness:.2f} N/mm, Mass={mass:.6f} kg")
print(f" Params: {trial.params}")
```
### Export Results
```python
# Export to CSV
import pandas as pd
df = study.trials_dataframe()
df.to_csv('2_results/trials.csv', index=False)
# Export to JSON
import json
with open('2_results/trials.json', 'w') as f:
json.dump([t.params | {'values': t.values} for t in study.trials], f, indent=2)
```
## Optimization Settings
- **Framework**: Protocol 10 - Intelligent Multi-Strategy Optimization (IMSO)
- **Adaptive Features**:
- Landscape characterization (analyzes problem structure)
- Strategy selection (picks best algorithm automatically)
- Dynamic switching (changes strategy when stagnating)
- Adaptive surrogate modeling
- **Strategies Available**: TPE, CMA-ES, QMC, Random, NSGA-II
- **Default trials**: 50
- **Parallelization**: 1 job (sequential)
### How Protocol 10 Works:
1. **Characterization Phase** (first 10 trials)
- Random sampling to explore landscape
- Analyzes: smoothness, multimodality, noise, dimensionality
2. **Strategy Selection**
- Automatically picks best optimizer based on landscape
- Example: Smooth → CMA-ES, Multimodal → TPE
3. **Adaptive Optimization**
- Monitors progress every 10 trials
- Switches strategies if stagnating
- All history kept for surrogate modeling
## Expected Performance
- **Trial duration**: ~2-5 minutes (depends on mesh size)
- **50 trials**: ~2-4 hours
- **Infeasibility rate**: ~20-30% (trials violating mass constraint, but kept for surrogate)
## Constraints
1. **Mass Constraint**: mass ≤ 0.2 kg
- Trials exceeding this are **NOT pruned** - they complete normally
- Kept in history for surrogate modeling (valuable search information)
- Marked as infeasible in database with `constraint_satisfied=False` attribute
- **Not eligible for Pareto front** - only feasible solutions reported as optimal
- This approach preserves knowledge while enforcing hard constraints
## Notes
- Simulation uses NX Nastran SOL 101 (linear static)
- Force units: Newtons (N)
- Displacement units: millimeters (mm)
- Mass units: kilograms (kg), converted from ton-mm-sec system
- Stiffness units: N/mm
## Troubleshooting
### NX Session Issues
If NX hangs or crashes:
```bash
# Kill all NX processes
taskkill /F /IM NXBIN.exe
```
### Extractor Failures
Check that:
- ResultProbe is defined in simulation for z-displacement
- OP2 file is generated (check solver settings)
- Field export journal has correct path
### Database Locked
If database is locked:
```bash
# Close all connections and restart
rm 2_results/study.db-journal
```
## References
- Generic extractors: `optimization_engine/extractors/`
- NX solver: `optimization_engine/nx_solver.py`
- pyNastran docs: https://pynastran-git.readthedocs.io/

View File

@@ -0,0 +1,242 @@
"""
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)

View File

@@ -0,0 +1,115 @@
"""
Diagnostic script to inspect OP2 file contents
Helps identify what data is available and what's missing
"""
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(project_root))
from pyNastran.op2.op2 import read_op2
# Path to OP2 file
op2_file = Path(__file__).parent / "1_setup" / "model" / "bracket_sim1-solution_1.op2"
if not op2_file.exists():
print(f"[ERROR] OP2 file not found: {op2_file}")
sys.exit(1)
print("="*70)
print("OP2 FILE DIAGNOSTIC")
print("="*70)
print(f"File: {op2_file}")
print(f"Size: {op2_file.stat().st_size:,} bytes")
print()
# Load OP2 file
print("[1/3] Loading OP2 file...")
op2 = read_op2(str(op2_file), debug=False)
print(" [OK] OP2 file loaded successfully")
print()
# Check for Grid Point Weight (Mass Properties)
print("[2/3] Checking for Grid Point Weight (GRDPNT)...")
if hasattr(op2, 'grid_point_weight'):
if op2.grid_point_weight:
gpw = op2.grid_point_weight
print(" [OK] Grid Point Weight data found!")
print()
print(" Available attributes:")
for attr in dir(gpw):
if not attr.startswith('_'):
print(f" - {attr}")
print()
# Check for MO matrix
if hasattr(gpw, 'MO'):
print(f" [OK] MO matrix found: {gpw.MO}")
if gpw.MO is not None and len(gpw.MO) > 0:
mass_ton = gpw.MO[0, 0]
mass_kg = mass_ton * 1000.0
print(f" [OK] Mass: {mass_kg:.6f} kg ({mass_ton:.6e} ton)")
else:
print(" [X] MO matrix is empty or None")
else:
print(" [X] MO matrix not found")
# Check for CG
if hasattr(gpw, 'cg'):
print(f" [OK] CG found: {gpw.cg}")
else:
print(" [!] CG not found")
# Check reference point
if hasattr(gpw, 'reference_point'):
print(f" [OK] Reference point: {gpw.reference_point}")
else:
print(" [!] Reference point not found")
else:
print(" [X] grid_point_weight attribute exists but is empty/None")
else:
print(" [X] No grid_point_weight attribute found")
print()
print(" GRDPNT output request is NOT enabled in Nastran")
print(" Solution:")
print(" 1. Open Bracket_fem1.fem in NX")
print(" 2. Enable GRDPNT in Case Control or Bulk Data")
print(" 3. Save the FEM file")
print(" 4. Re-run the optimization")
print()
# Check for Grid Point Forces
print("[3/3] Checking for Grid Point Forces (GPFORCE)...")
if hasattr(op2, 'grid_point_forces'):
if op2.grid_point_forces:
print(" [OK] Grid Point Forces data found!")
print(f" Subcases available: {list(op2.grid_point_forces.keys())}")
else:
print(" [X] grid_point_forces attribute exists but is empty")
else:
print(" [X] No grid_point_forces found (expected - using applied_force parameter)")
print()
print("="*70)
print("DIAGNOSIS COMPLETE")
print("="*70)
# Summary
print()
print("SUMMARY:")
has_mass = hasattr(op2, 'grid_point_weight') and op2.grid_point_weight
if has_mass:
print("[OK] Mass extraction should work")
else:
print("[X] Mass extraction will FAIL - GRDPNT not enabled")
print()
print("NEXT STEPS:")
print("1. In NX, open: studies/bracket_stiffness_optimization/1_setup/model/Bracket_fem1.fem")
print("2. Go to: File > Utilities > Customer Defaults")
print("3. Search for: GRDPNT")
print("4. OR: Add 'PARAM,GRDPNT,0' to Bulk Data section")
print("5. Save and close")
print("6. Re-run optimization")

View File

@@ -0,0 +1,81 @@
"""
Extract mass from NX measure expression in Bracket.prt
This script reads the mass value directly from the model's measure expression,
bypassing the need for GRDPNT output in the OP2 file.
"""
import sys
from pathlib import Path
import NXOpen
def extract_mass_from_prt(prt_file: Path) -> float:
"""
Extract mass from NX .prt file measure expression.
Args:
prt_file: Path to .prt file with mass measure expression
Returns:
Mass in kg
"""
theSession = NXOpen.Session.GetSession()
# Open the part file
print("[1/2] Opening part file: " + str(prt_file))
try:
basePart, partLoadStatus = theSession.Parts.OpenBaseDisplay(str(prt_file))
partLoadStatus.Dispose()
except Exception as e:
raise RuntimeError("Failed to open part file: " + str(e))
# Get all expressions
print("[2/2] Reading expressions...")
expressions = basePart.Expressions
# Search for mass expression (common names: "mass", "bracket_mass", "total_mass", etc.)
mass_value = None
mass_expr_name = None
for expr in expressions:
expr_name = expr.Name.lower()
if 'mass' in expr_name:
# Found a mass expression
mass_expr_name = expr.Name
mass_value = expr.Value
print(" Found mass expression: '" + expr.Name + "' = " + str(mass_value))
break
if mass_value is None:
# List all expressions to help debug
print("\n Available expressions:")
for expr in expressions:
print(" - " + expr.Name + " = " + str(expr.Value))
raise ValueError("No mass expression found in part file")
# Close the part
theSession.Parts.CloseAll(NXOpen.BasePart.CloseWholeTree.False, None)
print("\n[OK] Mass extracted: {:.6f} kg".format(mass_value))
return mass_value
if __name__ == "__main__":
if len(sys.argv) > 1:
prt_file = Path(sys.argv[1])
else:
# Default to Bracket.prt in same directory
prt_file = Path(__file__).parent / "Bracket.prt"
if not prt_file.exists():
print(f"ERROR: Part file not found: {prt_file}")
sys.exit(1)
try:
mass_kg = extract_mass_from_prt(prt_file)
print(f"\nMass: {mass_kg:.6f} kg ({mass_kg * 1000:.2f} g)")
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@@ -0,0 +1,100 @@
{
"study_name": "bracket_stiffness_optimization_V2",
"description": "Maximize bracket stiffness while minimizing mass (constraint: mass ≤ 0.2 kg)",
"objectives": [
{
"name": "stiffness",
"type": "maximize",
"description": "Structural stiffness (N/mm) calculated as Force/Displacement",
"target": null,
"weight": 1.0
},
{
"name": "mass",
"type": "minimize",
"description": "Total mass (kg) - secondary objective with hard constraint",
"target": null,
"weight": 0.1
}
],
"constraints": [
{
"name": "mass_limit",
"type": "less_than",
"value": 0.2,
"description": "Maximum allowable mass: 200 grams"
}
],
"design_variables": [
{
"name": "support_angle",
"type": "continuous",
"min": 20.0,
"max": 70.0,
"initial": 60.0,
"unit": "degrees",
"description": "Angle of support arm relative to base"
},
{
"name": "tip_thickness",
"type": "continuous",
"min": 30.0,
"max": 60.0,
"initial": 30.0,
"unit": "mm",
"description": "Thickness of bracket tip where load is applied"
}
],
"optimization_settings": {
"algorithm": "NSGA-II",
"n_trials": 50,
"n_jobs": 1,
"sampler": "TPESampler",
"pruner": "MedianPruner",
"pruner_settings": {
"n_startup_trials": 10,
"n_warmup_steps": 5,
"interval_steps": 1
},
"timeout": null,
"seed": 42
},
"simulation_settings": {
"solver": "NX_Nastran",
"solution_type": "SOL101",
"analysis_type": "Linear_Static",
"model_file": "1_setup/model/Bracket.prt",
"sim_file": "1_setup/model/Bracket_sim1.sim",
"fem_file": "1_setup/model/Bracket_fem1.fem"
},
"extraction_settings": {
"extractor_module": "bracket_stiffness_extractor",
"extractor_class": "BracketStiffnessExtractor",
"field_file": "export_field_dz.fld",
"op2_file": "Bracket_sim1.op2",
"force_component": "fz",
"displacement_component": "z",
"displacement_aggregation": "max_abs"
},
"output_settings": {
"results_dir": "2_results",
"database_name": "study.db",
"checkpoint_interval": 5,
"visualization": true,
"export_format": ["json", "csv", "parquet"]
},
"dashboard_settings": {
"enabled": true,
"port": 8000,
"realtime_updates": true,
"websocket": true
}
}

View File

@@ -0,0 +1,354 @@
"""
Bracket Stiffness Optimization - Intelligent Optimizer (Protocol 10)
====================================================================
Run multi-objective optimization using Protocol 10: Intelligent Multi-Strategy
Optimization (IMSO) to:
1. Maximize stiffness (k = F/δ)
2. Minimize mass (constraint: ≤ 0.2 kg)
Design Variables:
- support_angle: 30° - 90°
- tip_thickness: 15mm - 40mm
Usage:
python run_optimization.py [--trials N] [--dashboard]
"""
import sys
import json
import argparse
from pathlib import Path
from datetime import datetime
# Add project root to path
project_root = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(project_root))
import optuna
from optimization_engine.intelligent_optimizer import IntelligentOptimizer
from optimization_engine.nx_solver import NXSolver
from bracket_stiffness_extractor import BracketStiffnessExtractor
# Import central configuration
import config as atomizer_config
def load_config(config_file: Path) -> dict:
"""Load optimization configuration from JSON."""
with open(config_file, 'r') as f:
return json.load(f)
def create_objective_function(config: dict, study_dir: Path):
"""
Create the objective function for bracket optimization.
Returns a function that takes an Optuna trial and returns objectives.
"""
def objective(trial: optuna.Trial) -> tuple:
"""
Optimization objective function.
Args:
trial: Optuna trial object
Returns:
(stiffness_neg, mass): Tuple of objectives
- stiffness_neg: Negative stiffness (for minimization)
- mass: Mass in kg
"""
# Sample design variables
design_vars = {}
for dv in config['design_variables']:
design_vars[dv['name']] = trial.suggest_float(
dv['name'],
dv['min'],
dv['max']
)
print(f"\n{'='*60}")
print(f"Trial #{trial.number}")
print(f"{'='*60}")
print(f"Design Variables:")
for name, value in design_vars.items():
print(f" {name}: {value:.3f}")
# Model paths
model_dir = study_dir / "1_setup" / "model"
model_file = model_dir / "Bracket.prt"
sim_file = model_dir / "Bracket_sim1.sim"
# Initialize NX solver using central config
nx_solver = NXSolver(
nastran_version=atomizer_config.NX_VERSION,
timeout=atomizer_config.NASTRAN_TIMEOUT,
use_journal=True,
enable_session_management=True,
study_name="bracket_stiffness_optimization_V2"
)
# Run simulation with design variable updates
print(f"\nRunning simulation with updated design variables...")
try:
result = nx_solver.run_simulation(
sim_file=sim_file,
working_dir=model_dir,
expression_updates=design_vars,
solution_name=None # Solve all solutions in the .sim file
)
if not result['success']:
print(f"ERROR: Simulation failed: {result.get('errors', 'Unknown error')}")
raise optuna.exceptions.TrialPruned()
except Exception as e:
print(f"ERROR: Simulation failed: {e}")
raise optuna.exceptions.TrialPruned()
# Step 3: Extract results (stiffness and mass)
print(f"Extracting results...")
extractor = BracketStiffnessExtractor(model_dir=model_dir)
try:
results = extractor.extract_results()
except Exception as e:
print(f"ERROR: Extraction failed: {e}")
raise optuna.exceptions.TrialPruned()
stiffness = results['stiffness'] # N/mm
mass = results['mass'] # kg
mass_g = results['mass_g'] # grams
# Check constraint: mass ≤ 0.2 kg
mass_limit = config['constraints'][0]['value']
constraint_satisfied = mass <= mass_limit
# Store constraint status and all metrics
trial.set_user_attr("constraint_satisfied", constraint_satisfied)
trial.set_user_attr("mass_limit", mass_limit)
trial.set_user_attr("mass_violation", max(0, mass - mass_limit))
trial.set_user_attr("displacement", results['displacement'])
trial.set_user_attr("force", results['force'])
trial.set_user_attr("compliance", results['compliance'])
trial.set_user_attr("mass_g", mass_g)
if not constraint_satisfied:
print(f"\n[!] CONSTRAINT VIOLATED: Mass {mass:.6f} kg > {mass_limit} kg")
print(f" Trial will be kept for surrogate modeling but not eligible for Pareto front")
print(f"\n[OK] Trial Complete")
print(f" Stiffness: {stiffness:.2f} N/mm")
print(f" Mass: {mass:.6f} kg ({mass_g:.2f} g)")
print(f" Constraint: {'[OK] SATISFIED' if constraint_satisfied else '[X] VIOLATED'}")
# Return tuple of objectives
# Note: Optuna minimizes by default, so return negative for maximization
return -stiffness, mass # Maximize stiffness, minimize mass
return objective
def run_optimization(
config_file: Path,
study_dir: Path,
n_trials: int = None,
dashboard: bool = False
):
"""
Run bracket stiffness optimization using Protocol 10.
Args:
config_file: Path to optimization_config.json
study_dir: Path to study directory
n_trials: Number of trials (overrides config)
dashboard: Enable real-time dashboard
"""
# Load configuration
config = load_config(config_file)
study_name = config['study_name']
if n_trials is None:
n_trials = config['optimization_settings']['n_trials']
# Setup results directory
results_dir = study_dir / config['output_settings']['results_dir']
results_dir.mkdir(exist_ok=True)
# Extract design variable bounds
design_variables = {
dv['name']: (dv['min'], dv['max'])
for dv in config['design_variables']
}
print(f"\n{'='*60}")
print(f"BRACKET STIFFNESS OPTIMIZATION - PROTOCOL 10")
print(f"{'='*60}")
print(f"Study: {study_name}")
print(f"Trials: {n_trials}")
print(f"Objectives: Maximize stiffness, Minimize mass")
print(f"Constraint: Mass <= 0.2 kg (kept for surrogate, filtered from Pareto)")
print(f"Optimizer: Intelligent Multi-Strategy (Protocol 10)")
print(f"Results: {results_dir}")
print(f"{'='*60}\n")
# Create intelligent optimizer
intelligent_config = {
'intelligent_optimization': {
'enabled': True,
'min_analysis_trials': 10,
'stagnation_window': 10,
'min_improvement_threshold': 0.001,
'target_value': None, # No specific target for multi-objective
'enable_adaptive_surrogate': True,
'enable_strategy_switching': True
},
'optimization_settings': config['optimization_settings'],
'output_settings': config['output_settings']
}
optimizer = IntelligentOptimizer(
study_name=study_name,
study_dir=results_dir,
config=intelligent_config,
verbose=True
)
# Create objective function
objective_fn = create_objective_function(config, study_dir)
# Run optimization
start_time = datetime.now()
try:
results = optimizer.optimize(
objective_function=objective_fn,
design_variables=design_variables,
n_trials=n_trials,
target_value=None, # Multi-objective, no single target
directions=["minimize", "minimize"] # Minimize -stiffness (=maximize stiffness), minimize mass
)
except KeyboardInterrupt:
print("\n\nOptimization interrupted by user.")
results = None
end_time = datetime.now()
elapsed = (end_time - start_time).total_seconds()
# Print results summary
print(f"\n{'='*60}")
print(f"OPTIMIZATION COMPLETE")
print(f"{'='*60}")
if results:
print(f"\n[BEST] Best Solution Found:")
print(f" Stiffness: {-results['best_value'][0]:.2f} N/mm") # Convert back from negative
print(f" Mass: {results['best_value'][1]:.6f} kg")
print(f" Parameters: {results['best_params']}")
print(f"\n[STRATEGY] Strategy Performance:")
print(f" Final Strategy: {results.get('strategy_used', 'N/A')}")
if 'landscape_analysis' in results and results['landscape_analysis'] is not None:
print(f" Landscape Type: {results['landscape_analysis'].get('landscape_type', 'N/A')}")
# Access the Optuna study for detailed analysis
study = optimizer.study
if study:
completed_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]
pruned_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.PRUNED]
failed_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.FAIL]
# Count constraint violations
constraint_violated = [t for t in completed_trials if not t.user_attrs.get("constraint_satisfied", True)]
constraint_satisfied = [t for t in completed_trials if t.user_attrs.get("constraint_satisfied", True)]
print(f"\n[STATS] Trial Statistics:")
print(f" Total trials: {len(study.trials)}")
print(f" Completed: {len(completed_trials)}")
print(f" [OK] Feasible (constraint satisfied): {len(constraint_satisfied)}")
print(f" [X] Infeasible (constraint violated): {len(constraint_violated)}")
print(f" Pruned: {len(pruned_trials)}")
print(f" Failed: {len(failed_trials)}")
print(f" Elapsed time: {elapsed:.1f} seconds ({elapsed/60:.1f} minutes)")
# Get best trials (Pareto front) - filtered to only feasible solutions
try:
# Get all Pareto-optimal trials first
all_best_trials = study.best_trials
# Filter to only feasible solutions (constraint satisfied)
feasible_best_trials = [
t for t in all_best_trials
if t.user_attrs.get("constraint_satisfied", True)
]
print(f"\n[PARETO] Pareto Front Analysis:")
print(f" Pareto Front (all trials): {len(all_best_trials)} solutions")
print(f" Pareto Front (feasible only): {len(feasible_best_trials)} solutions")
if len(feasible_best_trials) > 0:
print(f"\n Top 5 Feasible Solutions:")
print(f" {'Trial':<8} {'Stiffness':<15} {'Mass':<15} {'Angle':<12} {'Thickness':<12}")
print(f" {'-' * 70}")
for i, trial in enumerate(feasible_best_trials[:5]):
stiff_neg, mass = trial.values
stiffness = -stiff_neg # Convert back to positive
angle = trial.params.get('support_angle', 0)
thickness = trial.params.get('tip_thickness', 0)
print(f" {trial.number:<8} {stiffness:<15.2f} {mass:<15.6f} {angle:<12.2f} {thickness:<12.2f}")
else:
print(f"\n [!] Warning: No feasible solutions found in Pareto front!")
except Exception as e:
print(f"\n Note: Could not retrieve Pareto front: {e}")
# Save summary
summary_file = results_dir / "optimization_summary.json"
summary = {
"study_name": study_name,
"optimizer": "Protocol 10 - Intelligent Multi-Strategy",
"n_trials": len(study.trials),
"completed_trials": len(completed_trials),
"feasible_trials": len(constraint_satisfied),
"infeasible_trials": len(constraint_violated),
"pruned_trials": len(pruned_trials),
"failed_trials": len(failed_trials),
"elapsed_seconds": elapsed,
"pareto_front_all": len(all_best_trials) if 'all_best_trials' in locals() else 0,
"pareto_front_feasible": len(feasible_best_trials) if 'feasible_best_trials' in locals() else 0,
"best_solution": results if results else None,
"timestamp": datetime.now().isoformat()
}
with open(summary_file, 'w') as f:
json.dump(summary, f, indent=2)
print(f"\n[SAVED] Summary saved to: {summary_file}")
print(f"\n{'='*60}\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run bracket stiffness optimization with Protocol 10")
parser.add_argument('--trials', type=int, default=None, help='Number of trials (default: from config)')
parser.add_argument('--dashboard', action='store_true', help='Enable real-time dashboard')
args = parser.parse_args()
# Paths
study_dir = Path(__file__).parent
config_file = study_dir / "optimization_config.json"
if not config_file.exists():
print(f"ERROR: Configuration file not found: {config_file}")
sys.exit(1)
# Run optimization
run_optimization(
config_file=config_file,
study_dir=study_dir,
n_trials=args.trials,
dashboard=args.dashboard
)

View File

@@ -0,0 +1,131 @@
{
"workflow_name": "bracket_stiffness_workflow",
"description": "End-to-end workflow for bracket stiffness optimization",
"version": "1.0",
"workflow_steps": [
{
"step": 1,
"name": "update_design_variables",
"description": "Update NX model expressions with trial parameters",
"action": "nx_update_expressions",
"inputs": {
"model_file": "1_setup/model/Bracket.prt",
"expressions": {
"support_angle": "{{support_angle}}",
"tip_thickness": "{{tip_thickness}}"
}
},
"outputs": ["updated_model"],
"on_failure": "abort_trial"
},
{
"step": 2,
"name": "solve_simulation",
"description": "Run NX Nastran SOL 101 linear static analysis",
"action": "nx_solve",
"inputs": {
"sim_file": "1_setup/model/Bracket_sim1.sim",
"solver": "NX_Nastran",
"solution": "SOL101"
},
"outputs": ["op2_file", "f06_file"],
"on_failure": "abort_trial",
"timeout": 600
},
{
"step": 3,
"name": "export_displacement_field",
"description": "Export z-displacement field from results",
"action": "nx_journal",
"inputs": {
"journal_file": "1_setup/model/export_displacement_field.py",
"sim_file": "1_setup/model/Bracket_sim1.sim"
},
"outputs": ["field_file"],
"on_failure": "abort_trial"
},
{
"step": 4,
"name": "extract_results",
"description": "Extract stiffness and mass from FEA results",
"action": "python_extractor",
"inputs": {
"extractor_script": "bracket_stiffness_extractor.py",
"field_file": "1_setup/model/export_field_dz.fld",
"op2_file": "1_setup/model/Bracket_sim1.op2"
},
"outputs": {
"stiffness": "objectives.stiffness",
"mass": "objectives.mass",
"displacement": "displacement",
"force": "force"
},
"on_failure": "abort_trial"
},
{
"step": 5,
"name": "evaluate_constraints",
"description": "Check mass constraint (≤ 0.2 kg) - mark as infeasible but keep for surrogate",
"action": "constraint_check",
"inputs": {
"mass": "{{mass}}",
"max_mass": 0.2
},
"outputs": ["constraint_satisfied"],
"on_failure": "continue"
},
{
"step": 6,
"name": "report_results",
"description": "Send results to optimization engine and dashboard",
"action": "report",
"inputs": {
"trial_number": "{{trial_number}}",
"objectives": {
"stiffness": "{{stiffness}}",
"mass": "{{mass}}"
},
"design_variables": {
"support_angle": "{{support_angle}}",
"tip_thickness": "{{tip_thickness}}"
},
"metadata": {
"displacement": "{{displacement}}",
"force": "{{force}}"
}
},
"outputs": ["trial_complete"],
"on_failure": "log_error"
}
],
"error_handling": {
"max_retries": 2,
"retry_delay": 5,
"fallback_action": "skip_trial",
"log_errors": true,
"error_log_file": "2_results/errors.log"
},
"cleanup": {
"delete_intermediate_files": false,
"archive_results": true,
"compress_op2": false
},
"logging": {
"level": "INFO",
"log_file": "2_results/workflow.log",
"console_output": true,
"log_rotation": {
"max_size_mb": 50,
"backup_count": 3
}
}
}