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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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"
|
||||
}
|
||||
BIN
studies/bracket_stiffness_optimization_V2/2_results/study.db
Normal file
BIN
studies/bracket_stiffness_optimization_V2/2_results/study.db
Normal file
Binary file not shown.
256
studies/bracket_stiffness_optimization_V2/README.md
Normal file
256
studies/bracket_stiffness_optimization_V2/README.md
Normal 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/
|
||||
@@ -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)
|
||||
115
studies/bracket_stiffness_optimization_V2/diagnose_op2.py
Normal file
115
studies/bracket_stiffness_optimization_V2/diagnose_op2.py
Normal 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")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
354
studies/bracket_stiffness_optimization_V2/run_optimization.py
Normal file
354
studies/bracket_stiffness_optimization_V2/run_optimization.py
Normal 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
|
||||
)
|
||||
131
studies/bracket_stiffness_optimization_V2/workflow_config.json
Normal file
131
studies/bracket_stiffness_optimization_V2/workflow_config.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user