feat: Add MLP surrogate with Turbo Mode for 100x faster optimization
Neural Acceleration (MLP Surrogate): - Add run_nn_optimization.py with hybrid FEA/NN workflow - MLP architecture: 4-layer (64->128->128->64) with BatchNorm/Dropout - Three workflow modes: - --all: Sequential export->train->optimize->validate - --hybrid-loop: Iterative Train->NN->Validate->Retrain cycle - --turbo: Aggressive single-best validation (RECOMMENDED) - Turbo mode: 5000 NN trials + 50 FEA validations in ~12 minutes - Separate nn_study.db to avoid overloading dashboard Performance Results (bracket_pareto_3obj study): - NN prediction errors: mass 1-5%, stress 1-4%, stiffness 5-15% - Found minimum mass designs at boundary (angle~30deg, thick~30mm) - 100x speedup vs pure FEA exploration Protocol Operating System: - Add .claude/skills/ with Bootstrap, Cheatsheet, Context Loader - Add docs/protocols/ with operations (OP_01-06) and system (SYS_10-14) - Update SYS_14_NEURAL_ACCELERATION.md with MLP Turbo Mode docs NX Automation: - Add optimization_engine/hooks/ for NX CAD/CAE automation - Add study_wizard.py for guided study creation - Fix FEM mesh update: load idealized part before UpdateFemodel() New Study: - bracket_pareto_3obj: 3-objective Pareto (mass, stress, stiffness) - 167 FEA trials + 5000 NN trials completed - Demonstrates full hybrid workflow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
206
.claude/skills/00_BOOTSTRAP.md
Normal file
206
.claude/skills/00_BOOTSTRAP.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# Atomizer LLM Bootstrap
|
||||||
|
|
||||||
|
**Version**: 1.0
|
||||||
|
**Purpose**: First file any LLM session reads. Provides instant orientation and task routing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Orientation (30 Seconds)
|
||||||
|
|
||||||
|
**Atomizer** = LLM-first FEA optimization framework using NX Nastran + Optuna + Neural Networks.
|
||||||
|
|
||||||
|
**Your Role**: Help users set up, run, and analyze structural optimization studies through conversation.
|
||||||
|
|
||||||
|
**Core Philosophy**: "Talk, don't click." Users describe what they want; you configure and execute.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Classification Tree
|
||||||
|
|
||||||
|
When a user request arrives, classify it:
|
||||||
|
|
||||||
|
```
|
||||||
|
User Request
|
||||||
|
│
|
||||||
|
├─► CREATE something?
|
||||||
|
│ ├─ "new study", "set up", "create", "optimize this"
|
||||||
|
│ └─► Load: OP_01_CREATE_STUDY.md + core/study-creation-core.md
|
||||||
|
│
|
||||||
|
├─► RUN something?
|
||||||
|
│ ├─ "start", "run", "execute", "begin optimization"
|
||||||
|
│ └─► Load: OP_02_RUN_OPTIMIZATION.md
|
||||||
|
│
|
||||||
|
├─► CHECK status?
|
||||||
|
│ ├─ "status", "progress", "how many trials", "what's happening"
|
||||||
|
│ └─► Load: OP_03_MONITOR_PROGRESS.md
|
||||||
|
│
|
||||||
|
├─► ANALYZE results?
|
||||||
|
│ ├─ "results", "best design", "compare", "pareto"
|
||||||
|
│ └─► Load: OP_04_ANALYZE_RESULTS.md
|
||||||
|
│
|
||||||
|
├─► DEBUG/FIX error?
|
||||||
|
│ ├─ "error", "failed", "not working", "crashed"
|
||||||
|
│ └─► Load: OP_06_TROUBLESHOOT.md
|
||||||
|
│
|
||||||
|
├─► CONFIGURE settings?
|
||||||
|
│ ├─ "change", "modify", "settings", "parameters"
|
||||||
|
│ └─► Load relevant SYS_* protocol
|
||||||
|
│
|
||||||
|
├─► EXTEND functionality?
|
||||||
|
│ ├─ "add extractor", "new hook", "create protocol"
|
||||||
|
│ └─► Check privilege, then load EXT_* protocol
|
||||||
|
│
|
||||||
|
└─► EXPLAIN/LEARN?
|
||||||
|
├─ "what is", "how does", "explain"
|
||||||
|
└─► Load relevant SYS_* protocol for reference
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Routing Table
|
||||||
|
|
||||||
|
| User Intent | Keywords | Protocol | Skill to Load | Privilege |
|
||||||
|
|-------------|----------|----------|---------------|-----------|
|
||||||
|
| Create study | "new", "set up", "create", "optimize" | OP_01 | **create-study-wizard.md** | user |
|
||||||
|
| Run optimization | "start", "run", "execute", "begin" | OP_02 | - | user |
|
||||||
|
| Monitor progress | "status", "progress", "trials", "check" | OP_03 | - | user |
|
||||||
|
| Analyze results | "results", "best", "compare", "pareto" | OP_04 | - | user |
|
||||||
|
| Export training data | "export", "training data", "neural" | OP_05 | modules/neural-acceleration.md | user |
|
||||||
|
| Debug issues | "error", "failed", "not working", "help" | OP_06 | - | user |
|
||||||
|
| Understand IMSO | "protocol 10", "IMSO", "adaptive" | SYS_10 | - | user |
|
||||||
|
| Multi-objective | "pareto", "NSGA", "multi-objective" | SYS_11 | - | user |
|
||||||
|
| Extractors | "extractor", "displacement", "stress" | SYS_12 | modules/extractors-catalog.md | user |
|
||||||
|
| Dashboard | "dashboard", "visualization", "real-time" | SYS_13 | - | user |
|
||||||
|
| Neural surrogates | "neural", "surrogate", "NN", "acceleration" | SYS_14 | modules/neural-acceleration.md | user |
|
||||||
|
| Add extractor | "create extractor", "new physics" | EXT_01 | - | power_user |
|
||||||
|
| Add hook | "create hook", "lifecycle", "callback" | EXT_02 | - | power_user |
|
||||||
|
| Add protocol | "create protocol", "new protocol" | EXT_03 | - | admin |
|
||||||
|
| Add skill | "create skill", "new skill" | EXT_04 | - | admin |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Role Detection
|
||||||
|
|
||||||
|
Determine user's privilege level:
|
||||||
|
|
||||||
|
| Role | How to Detect | Can Do | Cannot Do |
|
||||||
|
|------|---------------|--------|-----------|
|
||||||
|
| **user** | Default for all sessions | Run studies, monitor, analyze, configure | Create extractors, modify protocols |
|
||||||
|
| **power_user** | User states they're a developer, or session context indicates | Create extractors, add hooks | Create protocols, modify skills |
|
||||||
|
| **admin** | Explicit declaration, admin config present | Full access | - |
|
||||||
|
|
||||||
|
**Default**: Assume `user` unless explicitly told otherwise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context Loading Rules
|
||||||
|
|
||||||
|
After classifying the task, load context in this order:
|
||||||
|
|
||||||
|
### 1. Always Loaded (via CLAUDE.md)
|
||||||
|
- This file (00_BOOTSTRAP.md)
|
||||||
|
- Python environment rules
|
||||||
|
- Code reuse protocol
|
||||||
|
|
||||||
|
### 2. Load Per Task Type
|
||||||
|
See `02_CONTEXT_LOADER.md` for complete loading rules.
|
||||||
|
|
||||||
|
**Quick Reference**:
|
||||||
|
```
|
||||||
|
CREATE_STUDY → create-study-wizard.md (PRIMARY)
|
||||||
|
→ Use: from optimization_engine.study_wizard import StudyWizard, create_study
|
||||||
|
→ modules/extractors-catalog.md (if asks about extractors)
|
||||||
|
→ modules/zernike-optimization.md (if telescope/mirror)
|
||||||
|
→ modules/neural-acceleration.md (if >50 trials)
|
||||||
|
|
||||||
|
RUN_OPTIMIZATION → OP_02_RUN_OPTIMIZATION.md
|
||||||
|
→ SYS_10_IMSO.md (if adaptive)
|
||||||
|
→ SYS_13_DASHBOARD_TRACKING.md (if monitoring)
|
||||||
|
|
||||||
|
DEBUG → OP_06_TROUBLESHOOT.md
|
||||||
|
→ Relevant SYS_* based on error type
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Framework
|
||||||
|
|
||||||
|
For ANY task, follow this pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. ANNOUNCE → State what you're about to do
|
||||||
|
2. VALIDATE → Check prerequisites are met
|
||||||
|
3. EXECUTE → Perform the action
|
||||||
|
4. VERIFY → Confirm success
|
||||||
|
5. REPORT → Summarize what was done
|
||||||
|
6. SUGGEST → Offer logical next steps
|
||||||
|
```
|
||||||
|
|
||||||
|
See `PROTOCOL_EXECUTION.md` for detailed execution rules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Emergency Quick Paths
|
||||||
|
|
||||||
|
### "I just want to run an optimization"
|
||||||
|
1. Do you have a `.prt` and `.sim` file? → Yes: OP_01 → OP_02
|
||||||
|
2. Getting errors? → OP_06
|
||||||
|
3. Want to see progress? → OP_03
|
||||||
|
|
||||||
|
### "Something broke"
|
||||||
|
1. Read the error message
|
||||||
|
2. Load OP_06_TROUBLESHOOT.md
|
||||||
|
3. Follow diagnostic flowchart
|
||||||
|
|
||||||
|
### "What did my optimization find?"
|
||||||
|
1. Load OP_04_ANALYZE_RESULTS.md
|
||||||
|
2. Query the study database
|
||||||
|
3. Generate report
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Directory Map
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/protocols/
|
||||||
|
├── operations/ # Layer 2: How-to guides
|
||||||
|
│ ├── OP_01_CREATE_STUDY.md
|
||||||
|
│ ├── OP_02_RUN_OPTIMIZATION.md
|
||||||
|
│ ├── OP_03_MONITOR_PROGRESS.md
|
||||||
|
│ ├── OP_04_ANALYZE_RESULTS.md
|
||||||
|
│ ├── OP_05_EXPORT_TRAINING_DATA.md
|
||||||
|
│ └── OP_06_TROUBLESHOOT.md
|
||||||
|
│
|
||||||
|
├── system/ # Layer 3: Core specifications
|
||||||
|
│ ├── SYS_10_IMSO.md
|
||||||
|
│ ├── SYS_11_MULTI_OBJECTIVE.md
|
||||||
|
│ ├── SYS_12_EXTRACTOR_LIBRARY.md
|
||||||
|
│ ├── SYS_13_DASHBOARD_TRACKING.md
|
||||||
|
│ └── SYS_14_NEURAL_ACCELERATION.md
|
||||||
|
│
|
||||||
|
└── extensions/ # Layer 4: Extensibility guides
|
||||||
|
├── EXT_01_CREATE_EXTRACTOR.md
|
||||||
|
├── EXT_02_CREATE_HOOK.md
|
||||||
|
├── EXT_03_CREATE_PROTOCOL.md
|
||||||
|
├── EXT_04_CREATE_SKILL.md
|
||||||
|
└── templates/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Constraints (Always Apply)
|
||||||
|
|
||||||
|
1. **Python Environment**: Always use `conda activate atomizer`
|
||||||
|
2. **Never modify master files**: Copy NX files to study working directory first
|
||||||
|
3. **Code reuse**: Check `optimization_engine/extractors/` before writing new extraction code
|
||||||
|
4. **Validation**: Always validate config before running optimization
|
||||||
|
5. **Documentation**: Every study needs README.md and STUDY_REPORT.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps After Bootstrap
|
||||||
|
|
||||||
|
1. If you know the task type → Go to relevant OP_* or SYS_* protocol
|
||||||
|
2. If unclear → Ask user clarifying question
|
||||||
|
3. If complex task → Read `01_CHEATSHEET.md` for quick reference
|
||||||
|
4. If need detailed loading rules → Read `02_CONTEXT_LOADER.md`
|
||||||
230
.claude/skills/01_CHEATSHEET.md
Normal file
230
.claude/skills/01_CHEATSHEET.md
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
# Atomizer Quick Reference Cheatsheet
|
||||||
|
|
||||||
|
**Version**: 1.0
|
||||||
|
**Purpose**: Rapid lookup for common operations. "I want X → Use Y"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task → Protocol Quick Lookup
|
||||||
|
|
||||||
|
| I want to... | Use Protocol | Key Command/Action |
|
||||||
|
|--------------|--------------|-------------------|
|
||||||
|
| Create a new optimization study | OP_01 | Generate `optimization_config.json` + `run_optimization.py` |
|
||||||
|
| Run an optimization | OP_02 | `conda activate atomizer && python run_optimization.py` |
|
||||||
|
| Check optimization progress | OP_03 | Query `study.db` or check dashboard at `localhost:3000` |
|
||||||
|
| See best results | OP_04 | `optuna-dashboard sqlite:///study.db` or dashboard |
|
||||||
|
| Export neural training data | OP_05 | `python run_optimization.py --export-training` |
|
||||||
|
| Fix an error | OP_06 | Read error log → follow diagnostic tree |
|
||||||
|
| Add custom physics extractor | EXT_01 | Create in `optimization_engine/extractors/` |
|
||||||
|
| Add lifecycle hook | EXT_02 | Create in `optimization_engine/plugins/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Extractor Quick Reference
|
||||||
|
|
||||||
|
| Physics | Extractor | Function Call |
|
||||||
|
|---------|-----------|---------------|
|
||||||
|
| Max displacement | E1 | `extract_displacement(op2_file, subcase=1)` |
|
||||||
|
| Natural frequency | E2 | `extract_frequency(op2_file, subcase=1, mode_number=1)` |
|
||||||
|
| Von Mises stress | E3 | `extract_solid_stress(op2_file, subcase=1, element_type='cquad4')` |
|
||||||
|
| BDF mass | E4 | `extract_mass_from_bdf(bdf_file)` |
|
||||||
|
| CAD expression mass | E5 | `extract_mass_from_expression(prt_file, expression_name='p173')` |
|
||||||
|
| Field data | E6 | `FieldDataExtractor(field_file, result_column, aggregation)` |
|
||||||
|
| Stiffness (k=F/δ) | E7 | `StiffnessCalculator(...)` |
|
||||||
|
| Zernike WFE | E8 | `extract_zernike_from_op2(op2_file, bdf_file, subcase)` |
|
||||||
|
| Zernike relative | E9 | `extract_zernike_relative_rms(op2_file, bdf_file, target, ref)` |
|
||||||
|
| Zernike builder | E10 | `ZernikeObjectiveBuilder(op2_finder)` |
|
||||||
|
| Part mass + material | E11 | `extract_part_mass_material(prt_file)` → mass, volume, material |
|
||||||
|
|
||||||
|
**Full details**: See `SYS_12_EXTRACTOR_LIBRARY.md` or `modules/extractors-catalog.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Selection Guide
|
||||||
|
|
||||||
|
### Single Objective Optimization
|
||||||
|
```
|
||||||
|
Question: Do you have ONE goal to minimize/maximize?
|
||||||
|
├─ Yes, simple problem (smooth, <10 params)
|
||||||
|
│ └─► Protocol 10 + CMA-ES or GP-BO sampler
|
||||||
|
│
|
||||||
|
├─ Yes, complex problem (noisy, many params)
|
||||||
|
│ └─► Protocol 10 + TPE sampler
|
||||||
|
│
|
||||||
|
└─ Not sure about problem characteristics?
|
||||||
|
└─► Protocol 10 with adaptive characterization (default)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Objective Optimization
|
||||||
|
```
|
||||||
|
Question: Do you have 2-3 competing goals?
|
||||||
|
├─ Yes (e.g., minimize mass AND minimize stress)
|
||||||
|
│ └─► Protocol 11 + NSGA-II sampler
|
||||||
|
│
|
||||||
|
└─ Pareto front needed?
|
||||||
|
└─► Protocol 11 (returns best_trials, not best_trial)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neural Network Acceleration
|
||||||
|
```
|
||||||
|
Question: Do you need >50 trials OR surrogate model?
|
||||||
|
├─ Yes
|
||||||
|
│ └─► Protocol 14 (configure surrogate_settings in config)
|
||||||
|
│
|
||||||
|
└─ Training data export needed?
|
||||||
|
└─► OP_05_EXPORT_TRAINING_DATA.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Quick Reference
|
||||||
|
|
||||||
|
### optimization_config.json Structure
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"study_name": "my_study",
|
||||||
|
"design_variables": [
|
||||||
|
{"name": "thickness", "min": 1.0, "max": 10.0, "unit": "mm"}
|
||||||
|
],
|
||||||
|
"objectives": [
|
||||||
|
{"name": "mass", "goal": "minimize", "unit": "kg"}
|
||||||
|
],
|
||||||
|
"constraints": [
|
||||||
|
{"name": "max_stress", "type": "<=", "threshold": 250, "unit": "MPa"}
|
||||||
|
],
|
||||||
|
"optimization_settings": {
|
||||||
|
"protocol": "protocol_10_single_objective",
|
||||||
|
"sampler": "TPESampler",
|
||||||
|
"n_trials": 50
|
||||||
|
},
|
||||||
|
"simulation": {
|
||||||
|
"model_file": "model.prt",
|
||||||
|
"sim_file": "model.sim",
|
||||||
|
"solver": "nastran"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sampler Quick Selection
|
||||||
|
| Sampler | Use When | Protocol |
|
||||||
|
|---------|----------|----------|
|
||||||
|
| `TPESampler` | Default, robust to noise | P10 |
|
||||||
|
| `CMAESSampler` | Smooth, unimodal problems | P10 |
|
||||||
|
| `GPSampler` | Expensive FEA, few trials | P10 |
|
||||||
|
| `NSGAIISampler` | Multi-objective (2-3 goals) | P11 |
|
||||||
|
| `RandomSampler` | Characterization phase only | P10 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Study File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
studies/{study_name}/
|
||||||
|
├── 1_setup/
|
||||||
|
│ ├── model/ # NX files (.prt, .sim, .fem)
|
||||||
|
│ └── optimization_config.json
|
||||||
|
├── 2_results/
|
||||||
|
│ ├── study.db # Optuna SQLite database
|
||||||
|
│ ├── optimizer_state.json # Real-time state (P13)
|
||||||
|
│ └── trial_logs/
|
||||||
|
├── README.md # MANDATORY: Engineering blueprint
|
||||||
|
├── STUDY_REPORT.md # MANDATORY: Results tracking
|
||||||
|
└── run_optimization.py # Entrypoint script
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Activate environment (ALWAYS FIRST)
|
||||||
|
conda activate atomizer
|
||||||
|
|
||||||
|
# Run optimization
|
||||||
|
python run_optimization.py
|
||||||
|
|
||||||
|
# Run with specific trial count
|
||||||
|
python run_optimization.py --n-trials 100
|
||||||
|
|
||||||
|
# Resume interrupted optimization
|
||||||
|
python run_optimization.py --resume
|
||||||
|
|
||||||
|
# Export training data for neural network
|
||||||
|
python run_optimization.py --export-training
|
||||||
|
|
||||||
|
# View results in Optuna dashboard
|
||||||
|
optuna-dashboard sqlite:///2_results/study.db
|
||||||
|
|
||||||
|
# Check study status
|
||||||
|
python -c "import optuna; s=optuna.load_study('my_study', 'sqlite:///2_results/study.db'); print(f'Trials: {len(s.trials)}')"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Quick Fixes
|
||||||
|
|
||||||
|
| Error | Likely Cause | Quick Fix |
|
||||||
|
|-------|--------------|-----------|
|
||||||
|
| "No module named optuna" | Wrong environment | `conda activate atomizer` |
|
||||||
|
| "NX session timeout" | Model too complex | Increase `timeout` in config |
|
||||||
|
| "OP2 file not found" | Solve failed | Check NX log for errors |
|
||||||
|
| "No feasible solutions" | Constraints too tight | Relax constraint thresholds |
|
||||||
|
| "NSGA-II requires >1 objective" | Wrong protocol | Use P10 for single-objective |
|
||||||
|
| "Expression not found" | Wrong parameter name | Verify expression names in NX |
|
||||||
|
| **All trials identical results** | **Missing `*_i.prt`** | **Copy idealized part to study folder!** |
|
||||||
|
|
||||||
|
**Full troubleshooting**: See `OP_06_TROUBLESHOOT.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRITICAL: NX FEM Mesh Update
|
||||||
|
|
||||||
|
**If all optimization trials produce identical results, the mesh is NOT updating!**
|
||||||
|
|
||||||
|
### Required Files for Mesh Updates
|
||||||
|
```
|
||||||
|
studies/{study}/1_setup/model/
|
||||||
|
├── Model.prt # Geometry
|
||||||
|
├── Model_fem1_i.prt # Idealized part ← MUST EXIST!
|
||||||
|
├── Model_fem1.fem # FEM
|
||||||
|
└── Model_sim1.sim # Simulation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why It Matters
|
||||||
|
The `*_i.prt` (idealized part) MUST be:
|
||||||
|
1. **Present** in the study folder
|
||||||
|
2. **Loaded** before `UpdateFemodel()` (already implemented in `solve_simulation.py`)
|
||||||
|
|
||||||
|
Without it, `UpdateFemodel()` runs but the mesh doesn't change!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Privilege Levels
|
||||||
|
|
||||||
|
| Level | Can Create Studies | Can Add Extractors | Can Add Protocols |
|
||||||
|
|-------|-------------------|-------------------|------------------|
|
||||||
|
| user | ✓ | ✗ | ✗ |
|
||||||
|
| power_user | ✓ | ✓ | ✗ |
|
||||||
|
| admin | ✓ | ✓ | ✓ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dashboard URLs
|
||||||
|
|
||||||
|
| Service | URL | Purpose |
|
||||||
|
|---------|-----|---------|
|
||||||
|
| Atomizer Dashboard | `http://localhost:3000` | Real-time optimization monitoring |
|
||||||
|
| Optuna Dashboard | `http://localhost:8080` | Trial history, parameter importance |
|
||||||
|
| API Backend | `http://localhost:5000` | REST API for dashboard |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Numbers Reference
|
||||||
|
|
||||||
|
| # | Name | Purpose |
|
||||||
|
|---|------|---------|
|
||||||
|
| 10 | IMSO | Intelligent Multi-Strategy Optimization (adaptive) |
|
||||||
|
| 11 | Multi-Objective | NSGA-II for Pareto optimization |
|
||||||
|
| 12 | - | (Reserved) |
|
||||||
|
| 13 | Dashboard | Real-time tracking and visualization |
|
||||||
|
| 14 | Neural | Surrogate model acceleration |
|
||||||
308
.claude/skills/02_CONTEXT_LOADER.md
Normal file
308
.claude/skills/02_CONTEXT_LOADER.md
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
# Atomizer Context Loader
|
||||||
|
|
||||||
|
**Version**: 1.0
|
||||||
|
**Purpose**: Define what documentation to load based on task type. Ensures LLM sessions have exactly the context needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context Loading Philosophy
|
||||||
|
|
||||||
|
1. **Minimal by default**: Don't load everything; load what's needed
|
||||||
|
2. **Expand on demand**: Load additional modules when signals detected
|
||||||
|
3. **Single source of truth**: Each concept defined in ONE place
|
||||||
|
4. **Layer progression**: Bootstrap → Operations → System → Extensions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task-Based Loading Rules
|
||||||
|
|
||||||
|
### CREATE_STUDY
|
||||||
|
|
||||||
|
**Trigger Keywords**: "new", "set up", "create", "optimize", "study"
|
||||||
|
|
||||||
|
**Always Load**:
|
||||||
|
```
|
||||||
|
.claude/skills/core/study-creation-core.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Load If**:
|
||||||
|
| Condition | Load |
|
||||||
|
|-----------|------|
|
||||||
|
| User asks about extractors | `modules/extractors-catalog.md` |
|
||||||
|
| Telescope/mirror/optics mentioned | `modules/zernike-optimization.md` |
|
||||||
|
| >50 trials OR "neural" OR "surrogate" | `modules/neural-acceleration.md` |
|
||||||
|
| Multi-objective (2+ goals) | `docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md` |
|
||||||
|
|
||||||
|
**Example Context Stack**:
|
||||||
|
```
|
||||||
|
# Simple bracket optimization
|
||||||
|
core/study-creation-core.md
|
||||||
|
|
||||||
|
# Mirror optimization with neural acceleration
|
||||||
|
core/study-creation-core.md
|
||||||
|
modules/zernike-optimization.md
|
||||||
|
modules/neural-acceleration.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### RUN_OPTIMIZATION
|
||||||
|
|
||||||
|
**Trigger Keywords**: "start", "run", "execute", "begin", "launch"
|
||||||
|
|
||||||
|
**Always Load**:
|
||||||
|
```
|
||||||
|
docs/protocols/operations/OP_02_RUN_OPTIMIZATION.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Load If**:
|
||||||
|
| Condition | Load |
|
||||||
|
|-----------|------|
|
||||||
|
| "adaptive" OR "characterization" | `docs/protocols/system/SYS_10_IMSO.md` |
|
||||||
|
| "dashboard" OR "real-time" | `docs/protocols/system/SYS_13_DASHBOARD_TRACKING.md` |
|
||||||
|
| "resume" OR "continue" | OP_02 has resume section |
|
||||||
|
| Errors occur | `docs/protocols/operations/OP_06_TROUBLESHOOT.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MONITOR_PROGRESS
|
||||||
|
|
||||||
|
**Trigger Keywords**: "status", "progress", "how many", "trials", "check"
|
||||||
|
|
||||||
|
**Always Load**:
|
||||||
|
```
|
||||||
|
docs/protocols/operations/OP_03_MONITOR_PROGRESS.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Load If**:
|
||||||
|
| Condition | Load |
|
||||||
|
|-----------|------|
|
||||||
|
| Dashboard questions | `docs/protocols/system/SYS_13_DASHBOARD_TRACKING.md` |
|
||||||
|
| Pareto/multi-objective | `docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ANALYZE_RESULTS
|
||||||
|
|
||||||
|
**Trigger Keywords**: "results", "best", "compare", "pareto", "report"
|
||||||
|
|
||||||
|
**Always Load**:
|
||||||
|
```
|
||||||
|
docs/protocols/operations/OP_04_ANALYZE_RESULTS.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Load If**:
|
||||||
|
| Condition | Load |
|
||||||
|
|-----------|------|
|
||||||
|
| Multi-objective/Pareto | `docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md` |
|
||||||
|
| Surrogate accuracy | `docs/protocols/system/SYS_14_NEURAL_ACCELERATION.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### EXPORT_TRAINING_DATA
|
||||||
|
|
||||||
|
**Trigger Keywords**: "export", "training data", "neural network data"
|
||||||
|
|
||||||
|
**Always Load**:
|
||||||
|
```
|
||||||
|
docs/protocols/operations/OP_05_EXPORT_TRAINING_DATA.md
|
||||||
|
modules/neural-acceleration.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TROUBLESHOOT
|
||||||
|
|
||||||
|
**Trigger Keywords**: "error", "failed", "not working", "crashed", "help"
|
||||||
|
|
||||||
|
**Always Load**:
|
||||||
|
```
|
||||||
|
docs/protocols/operations/OP_06_TROUBLESHOOT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Load If**:
|
||||||
|
| Error Type | Load |
|
||||||
|
|------------|------|
|
||||||
|
| NX/solve errors | NX solver section of core skill |
|
||||||
|
| Extractor errors | `modules/extractors-catalog.md` |
|
||||||
|
| Dashboard errors | `docs/protocols/system/SYS_13_DASHBOARD_TRACKING.md` |
|
||||||
|
| Neural errors | `docs/protocols/system/SYS_14_NEURAL_ACCELERATION.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### UNDERSTAND_PROTOCOL
|
||||||
|
|
||||||
|
**Trigger Keywords**: "what is", "how does", "explain", "protocol"
|
||||||
|
|
||||||
|
**Load Based on Topic**:
|
||||||
|
| Topic | Load |
|
||||||
|
|-------|------|
|
||||||
|
| Protocol 10 / IMSO / adaptive | `docs/protocols/system/SYS_10_IMSO.md` |
|
||||||
|
| Protocol 11 / multi-objective / NSGA | `docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md` |
|
||||||
|
| Extractors / physics extraction | `docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md` |
|
||||||
|
| Protocol 13 / dashboard / real-time | `docs/protocols/system/SYS_13_DASHBOARD_TRACKING.md` |
|
||||||
|
| Protocol 14 / neural / surrogate | `docs/protocols/system/SYS_14_NEURAL_ACCELERATION.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### EXTEND_FUNCTIONALITY
|
||||||
|
|
||||||
|
**Trigger Keywords**: "create extractor", "add hook", "new protocol", "extend"
|
||||||
|
|
||||||
|
**Requires**: Privilege check first (see 00_BOOTSTRAP.md)
|
||||||
|
|
||||||
|
| Extension Type | Load | Privilege |
|
||||||
|
|----------------|------|-----------|
|
||||||
|
| New extractor | `docs/protocols/extensions/EXT_01_CREATE_EXTRACTOR.md` | power_user |
|
||||||
|
| New hook | `docs/protocols/extensions/EXT_02_CREATE_HOOK.md` | power_user |
|
||||||
|
| New protocol | `docs/protocols/extensions/EXT_03_CREATE_PROTOCOL.md` | admin |
|
||||||
|
| New skill | `docs/protocols/extensions/EXT_04_CREATE_SKILL.md` | admin |
|
||||||
|
|
||||||
|
**Always Load for Extractors**:
|
||||||
|
```
|
||||||
|
modules/nx-docs-lookup.md # NX API documentation via MCP
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NX_DEVELOPMENT
|
||||||
|
|
||||||
|
**Trigger Keywords**: "NX Open", "NXOpen", "NX API", "Simcenter", "Nastran card", "NX script"
|
||||||
|
|
||||||
|
**Always Load**:
|
||||||
|
```
|
||||||
|
modules/nx-docs-lookup.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**MCP Tools Available**:
|
||||||
|
| Tool | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `siemens_docs_search` | Search NX Open, Simcenter, Teamcenter docs |
|
||||||
|
| `siemens_docs_fetch` | Fetch specific documentation page |
|
||||||
|
| `siemens_auth_status` | Check Siemens SSO session status |
|
||||||
|
| `siemens_login` | Re-authenticate if session expired |
|
||||||
|
|
||||||
|
**Use When**:
|
||||||
|
- Building new extractors that use NX Open APIs
|
||||||
|
- Debugging NX automation errors
|
||||||
|
- Looking up Nastran card formats
|
||||||
|
- Finding correct method signatures
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signal Detection Patterns
|
||||||
|
|
||||||
|
Use these patterns to detect when to load additional modules:
|
||||||
|
|
||||||
|
### Zernike/Mirror Detection
|
||||||
|
```
|
||||||
|
Signals: "mirror", "telescope", "wavefront", "WFE", "Zernike",
|
||||||
|
"RMS", "polishing", "optical", "M1", "surface error"
|
||||||
|
|
||||||
|
Action: Load modules/zernike-optimization.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neural Acceleration Detection
|
||||||
|
```
|
||||||
|
Signals: "neural", "surrogate", "NN", "machine learning",
|
||||||
|
"acceleration", ">50 trials", "fast", "GNN"
|
||||||
|
|
||||||
|
Action: Load modules/neural-acceleration.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Objective Detection
|
||||||
|
```
|
||||||
|
Signals: Two or more objectives with different goals,
|
||||||
|
"pareto", "tradeoff", "NSGA", "multi-objective",
|
||||||
|
"minimize X AND maximize Y"
|
||||||
|
|
||||||
|
Action: Load SYS_11_MULTI_OBJECTIVE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### High-Complexity Detection
|
||||||
|
```
|
||||||
|
Signals: >10 design variables, "complex", "many parameters",
|
||||||
|
"adaptive", "characterization", "landscape"
|
||||||
|
|
||||||
|
Action: Load SYS_10_IMSO.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### NX Open / Simcenter Detection
|
||||||
|
```
|
||||||
|
Signals: "NX Open", "NXOpen", "NX API", "FemPart", "CAE.",
|
||||||
|
"Nastran", "CQUAD", "CTRIA", "MAT1", "PSHELL",
|
||||||
|
"mesh", "solver", "OP2", "BDF", "Simcenter"
|
||||||
|
|
||||||
|
Action: Load modules/nx-docs-lookup.md
|
||||||
|
Use MCP tools: siemens_docs_search, siemens_docs_fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context Stack Examples
|
||||||
|
|
||||||
|
### Example 1: Simple Bracket Optimization
|
||||||
|
```
|
||||||
|
User: "Help me optimize my bracket for minimum weight"
|
||||||
|
|
||||||
|
Load Stack:
|
||||||
|
1. core/study-creation-core.md # Core study creation logic
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Telescope Mirror with Neural
|
||||||
|
```
|
||||||
|
User: "I need to optimize my M1 mirror's wavefront error with 200 trials"
|
||||||
|
|
||||||
|
Load Stack:
|
||||||
|
1. core/study-creation-core.md # Core study creation
|
||||||
|
2. modules/zernike-optimization.md # Zernike-specific patterns
|
||||||
|
3. modules/neural-acceleration.md # Neural acceleration for 200 trials
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Multi-Objective Structural
|
||||||
|
```
|
||||||
|
User: "Minimize mass AND maximize stiffness for my beam"
|
||||||
|
|
||||||
|
Load Stack:
|
||||||
|
1. core/study-creation-core.md # Core study creation
|
||||||
|
2. SYS_11_MULTI_OBJECTIVE.md # Multi-objective protocol
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: Debug Session
|
||||||
|
```
|
||||||
|
User: "My optimization failed with NX timeout error"
|
||||||
|
|
||||||
|
Load Stack:
|
||||||
|
1. OP_06_TROUBLESHOOT.md # Troubleshooting guide
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 5: Create Custom Extractor
|
||||||
|
```
|
||||||
|
User: "I need to extract thermal gradients from my results"
|
||||||
|
|
||||||
|
Load Stack:
|
||||||
|
1. EXT_01_CREATE_EXTRACTOR.md # Extractor creation guide
|
||||||
|
2. modules/extractors-catalog.md # Reference existing patterns
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loading Priority Order
|
||||||
|
|
||||||
|
When multiple modules could apply, load in this order:
|
||||||
|
|
||||||
|
1. **Core skill** (always first for creation tasks)
|
||||||
|
2. **Primary operation protocol** (OP_*)
|
||||||
|
3. **Required system protocols** (SYS_*)
|
||||||
|
4. **Optional modules** (modules/*)
|
||||||
|
5. **Extension protocols** (EXT_*) - only if extending
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-Patterns (Don't Do)
|
||||||
|
|
||||||
|
1. **Don't load everything**: Only load what's needed for the task
|
||||||
|
2. **Don't load extensions for users**: Check privilege first
|
||||||
|
3. **Don't skip core skill**: For study creation, always load core first
|
||||||
|
4. **Don't mix incompatible protocols**: P10 (single-obj) vs P11 (multi-obj)
|
||||||
|
5. **Don't load deprecated docs**: Only use docs/protocols/* structure
|
||||||
398
.claude/skills/DEV_DOCUMENTATION.md
Normal file
398
.claude/skills/DEV_DOCUMENTATION.md
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
# Developer Documentation Skill
|
||||||
|
|
||||||
|
**Version**: 1.0
|
||||||
|
**Purpose**: Self-documenting system for Atomizer development. Use this skill to systematically document new features, protocols, extractors, and changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill enables **automatic documentation maintenance** during development. When you develop new features, use these commands to keep documentation in sync with code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Commands for Developers
|
||||||
|
|
||||||
|
### Document New Feature
|
||||||
|
|
||||||
|
**Tell Claude**:
|
||||||
|
```
|
||||||
|
"Document the new {feature} I just added"
|
||||||
|
```
|
||||||
|
|
||||||
|
Claude will:
|
||||||
|
1. Analyze the code changes
|
||||||
|
2. Determine which docs need updating
|
||||||
|
3. Update protocol files
|
||||||
|
4. Update CLAUDE.md if needed
|
||||||
|
5. Bump version numbers
|
||||||
|
6. Create changelog entry
|
||||||
|
|
||||||
|
### Document New Extractor
|
||||||
|
|
||||||
|
**Tell Claude**:
|
||||||
|
```
|
||||||
|
"I created a new extractor: extract_thermal.py. Document it."
|
||||||
|
```
|
||||||
|
|
||||||
|
Claude will:
|
||||||
|
1. Read the extractor code
|
||||||
|
2. Add entry to SYS_12_EXTRACTOR_LIBRARY.md
|
||||||
|
3. Add to extractors-catalog.md module
|
||||||
|
4. Update __init__.py exports
|
||||||
|
5. Create test file template
|
||||||
|
|
||||||
|
### Document Protocol Change
|
||||||
|
|
||||||
|
**Tell Claude**:
|
||||||
|
```
|
||||||
|
"I modified Protocol 10 to add {feature}. Update docs."
|
||||||
|
```
|
||||||
|
|
||||||
|
Claude will:
|
||||||
|
1. Read the code changes
|
||||||
|
2. Update SYS_10_IMSO.md
|
||||||
|
3. Bump version number
|
||||||
|
4. Add to Version History
|
||||||
|
5. Update cross-references
|
||||||
|
|
||||||
|
### Full Documentation Audit
|
||||||
|
|
||||||
|
**Tell Claude**:
|
||||||
|
```
|
||||||
|
"Audit documentation for {component/study/protocol}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Claude will:
|
||||||
|
1. Check all related docs
|
||||||
|
2. Identify stale content
|
||||||
|
3. Flag missing documentation
|
||||||
|
4. Suggest updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Workflow
|
||||||
|
|
||||||
|
### When You Add Code
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 1. WRITE CODE │
|
||||||
|
│ - New extractor, hook, or feature │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 2. TELL CLAUDE │
|
||||||
|
│ "Document the new {feature} I added" │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 3. CLAUDE UPDATES │
|
||||||
|
│ - Protocol files │
|
||||||
|
│ - Skill modules │
|
||||||
|
│ - Version numbers │
|
||||||
|
│ - Cross-references │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 4. REVIEW & COMMIT │
|
||||||
|
│ - Review changes │
|
||||||
|
│ - Commit code + docs together │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Update Rules
|
||||||
|
|
||||||
|
### File → Document Mapping
|
||||||
|
|
||||||
|
| If You Change... | Update These Docs |
|
||||||
|
|------------------|-------------------|
|
||||||
|
| `optimization_engine/extractors/*` | SYS_12, extractors-catalog.md |
|
||||||
|
| `optimization_engine/intelligent_optimizer.py` | SYS_10_IMSO.md |
|
||||||
|
| `optimization_engine/plugins/*` | EXT_02_CREATE_HOOK.md |
|
||||||
|
| `atomizer-dashboard/*` | SYS_13_DASHBOARD_TRACKING.md |
|
||||||
|
| `atomizer-field/*` | SYS_14_NEURAL_ACCELERATION.md |
|
||||||
|
| Any multi-objective code | SYS_11_MULTI_OBJECTIVE.md |
|
||||||
|
| Study creation workflow | OP_01_CREATE_STUDY.md |
|
||||||
|
| Run workflow | OP_02_RUN_OPTIMIZATION.md |
|
||||||
|
|
||||||
|
### Version Bumping Rules
|
||||||
|
|
||||||
|
| Change Type | Version Bump | Example |
|
||||||
|
|-------------|--------------|---------|
|
||||||
|
| Bug fix | Patch (+0.0.1) | 1.0.0 → 1.0.1 |
|
||||||
|
| New feature (backwards compatible) | Minor (+0.1.0) | 1.0.0 → 1.1.0 |
|
||||||
|
| Breaking change | Major (+1.0.0) | 1.0.0 → 2.0.0 |
|
||||||
|
|
||||||
|
### Required Updates for New Extractor
|
||||||
|
|
||||||
|
1. **SYS_12_EXTRACTOR_LIBRARY.md**:
|
||||||
|
- Add to Quick Reference table (assign E{N} ID)
|
||||||
|
- Add detailed section with code example
|
||||||
|
|
||||||
|
2. **skills/modules/extractors-catalog.md** (when created):
|
||||||
|
- Add entry with copy-paste code snippet
|
||||||
|
|
||||||
|
3. **optimization_engine/extractors/__init__.py**:
|
||||||
|
- Add import and export
|
||||||
|
|
||||||
|
4. **Tests**:
|
||||||
|
- Create `tests/test_extract_{name}.py`
|
||||||
|
|
||||||
|
### Required Updates for New Protocol
|
||||||
|
|
||||||
|
1. **docs/protocols/system/SYS_{N}_{NAME}.md**:
|
||||||
|
- Create full protocol document
|
||||||
|
|
||||||
|
2. **docs/protocols/README.md**:
|
||||||
|
- Add to navigation tables
|
||||||
|
|
||||||
|
3. **.claude/skills/01_CHEATSHEET.md**:
|
||||||
|
- Add to quick lookup table
|
||||||
|
|
||||||
|
4. **.claude/skills/02_CONTEXT_LOADER.md**:
|
||||||
|
- Add loading rules
|
||||||
|
|
||||||
|
5. **CLAUDE.md**:
|
||||||
|
- Add reference if major feature
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Documentation Commands
|
||||||
|
|
||||||
|
### "Document this change"
|
||||||
|
|
||||||
|
Claude analyzes recent changes and updates relevant docs.
|
||||||
|
|
||||||
|
**Input**: Description of what you changed
|
||||||
|
**Output**: Updated protocol files, version bumps, changelog
|
||||||
|
|
||||||
|
### "Create protocol for {feature}"
|
||||||
|
|
||||||
|
Claude creates a new protocol document following the template.
|
||||||
|
|
||||||
|
**Input**: Feature name and description
|
||||||
|
**Output**: New SYS_* or EXT_* document
|
||||||
|
|
||||||
|
### "Verify documentation for {component}"
|
||||||
|
|
||||||
|
Claude checks that docs match code.
|
||||||
|
|
||||||
|
**Input**: Component name
|
||||||
|
**Output**: List of discrepancies and suggested fixes
|
||||||
|
|
||||||
|
### "Generate changelog since {date/commit}"
|
||||||
|
|
||||||
|
Claude creates a changelog from git history.
|
||||||
|
|
||||||
|
**Input**: Date or commit reference
|
||||||
|
**Output**: Formatted changelog
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Document Template
|
||||||
|
|
||||||
|
When creating new protocols, use this structure:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# {LAYER}_{NUMBER}_{NAME}.md
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: {Full Name}
|
||||||
|
LAYER: {Operations|System|Extensions}
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: {YYYY-MM-DD}
|
||||||
|
PRIVILEGE: {user|power_user|admin}
|
||||||
|
LOAD_WITH: [{dependencies}]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
{1-3 sentence description}
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
{Tables, key parameters}
|
||||||
|
|
||||||
|
## Detailed Specification
|
||||||
|
{Full content}
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
{Working examples}
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
- Depends On: []
|
||||||
|
- Used By: []
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | {DATE} | Initial release |
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog Format
|
||||||
|
|
||||||
|
When updating protocols, add to Version History:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.2.0 | 2025-12-05 | Added thermal gradient support |
|
||||||
|
| 1.1.0 | 2025-12-01 | Improved error handling |
|
||||||
|
| 1.0.0 | 2025-11-20 | Initial release |
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Git
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
|
||||||
|
When documenting:
|
||||||
|
```
|
||||||
|
docs: Update SYS_12 with thermal extractor
|
||||||
|
|
||||||
|
- Added E11 thermal gradient extractor
|
||||||
|
- Updated Quick Reference table
|
||||||
|
- Added code example in Detailed section
|
||||||
|
- Bumped version to 1.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-Commit Checklist
|
||||||
|
|
||||||
|
Before committing code changes:
|
||||||
|
- [ ] Related docs updated
|
||||||
|
- [ ] Version numbers bumped
|
||||||
|
- [ ] Cross-references valid
|
||||||
|
- [ ] Examples tested
|
||||||
|
- [ ] CLAUDE.md updated if major feature
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: Document New Extractor
|
||||||
|
|
||||||
|
**You say**:
|
||||||
|
```
|
||||||
|
"I just created extract_thermal_gradient.py in optimization_engine/extractors/.
|
||||||
|
It extracts thermal gradients from temperature fields. Document it."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Claude does**:
|
||||||
|
|
||||||
|
1. Reads `extract_thermal_gradient.py`
|
||||||
|
2. Updates `SYS_12_EXTRACTOR_LIBRARY.md`:
|
||||||
|
```markdown
|
||||||
|
| E11 | Thermal Gradient | `extract_thermal_gradient()` | .op2 | K/mm |
|
||||||
|
```
|
||||||
|
3. Adds detailed section with code example
|
||||||
|
4. Updates `__init__.py`:
|
||||||
|
```python
|
||||||
|
from .extract_thermal_gradient import extract_thermal_gradient
|
||||||
|
__all__ = [..., 'extract_thermal_gradient']
|
||||||
|
```
|
||||||
|
5. Bumps SYS_12 version: 1.0 → 1.1
|
||||||
|
6. Reports what was updated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: Document Protocol Change
|
||||||
|
|
||||||
|
**You say**:
|
||||||
|
```
|
||||||
|
"I added a new stopping criterion to Protocol 10 based on improvement rate.
|
||||||
|
Update the documentation."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Claude does**:
|
||||||
|
|
||||||
|
1. Reads relevant code changes
|
||||||
|
2. Updates `SYS_10_IMSO.md`:
|
||||||
|
- Adds to Configuration section
|
||||||
|
- Updates Architecture diagram if needed
|
||||||
|
- Adds to Quick Reference
|
||||||
|
3. Bumps version: 2.1 → 2.2
|
||||||
|
4. Adds Version History entry:
|
||||||
|
```markdown
|
||||||
|
| 2.2 | 2025-12-05 | Added improvement rate stopping criterion |
|
||||||
|
```
|
||||||
|
5. Updates cross-references if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keeping Docs in Sync
|
||||||
|
|
||||||
|
### Daily Development
|
||||||
|
|
||||||
|
```
|
||||||
|
Morning: Start coding
|
||||||
|
│
|
||||||
|
├─► Write new feature
|
||||||
|
│
|
||||||
|
├─► Test feature
|
||||||
|
│
|
||||||
|
├─► "Claude, document the {feature} I just added"
|
||||||
|
│
|
||||||
|
└─► Commit code + docs together
|
||||||
|
```
|
||||||
|
|
||||||
|
### Weekly Audit
|
||||||
|
|
||||||
|
```
|
||||||
|
Friday:
|
||||||
|
│
|
||||||
|
├─► "Claude, audit documentation for recent changes"
|
||||||
|
│
|
||||||
|
├─► Review flagged issues
|
||||||
|
│
|
||||||
|
└─► Fix any stale documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release Preparation
|
||||||
|
|
||||||
|
```
|
||||||
|
Before release:
|
||||||
|
│
|
||||||
|
├─► "Claude, generate changelog since last release"
|
||||||
|
│
|
||||||
|
├─► "Claude, verify all protocol versions are consistent"
|
||||||
|
│
|
||||||
|
└─► Final review and version bump
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**To keep documentation in sync**:
|
||||||
|
|
||||||
|
1. **After coding**: Tell Claude what you changed
|
||||||
|
2. **Be specific**: "I added X to Y" works better than "update docs"
|
||||||
|
3. **Commit together**: Code and docs in same commit
|
||||||
|
4. **Regular audits**: Weekly check for stale docs
|
||||||
|
|
||||||
|
**Claude handles**:
|
||||||
|
- Finding which docs need updates
|
||||||
|
- Following the template structure
|
||||||
|
- Version bumping
|
||||||
|
- Cross-reference updates
|
||||||
|
- Changelog generation
|
||||||
|
|
||||||
|
**You handle**:
|
||||||
|
- Telling Claude what changed
|
||||||
|
- Reviewing Claude's updates
|
||||||
|
- Final commit
|
||||||
361
.claude/skills/PROTOCOL_EXECUTION.md
Normal file
361
.claude/skills/PROTOCOL_EXECUTION.md
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
# Protocol Execution Framework (PEF)
|
||||||
|
|
||||||
|
**Version**: 1.0
|
||||||
|
**Purpose**: Meta-protocol defining how LLM sessions execute Atomizer protocols. The "protocol for using protocols."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Execution Pattern
|
||||||
|
|
||||||
|
For ANY task, follow this 6-step pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 1. ANNOUNCE │
|
||||||
|
│ State what you're about to do in plain language │
|
||||||
|
│ "I'll create an optimization study for your bracket..." │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 2. VALIDATE │
|
||||||
|
│ Check prerequisites are met │
|
||||||
|
│ - Required files exist? │
|
||||||
|
│ - Environment ready? │
|
||||||
|
│ - User has confirmed understanding? │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 3. EXECUTE │
|
||||||
|
│ Perform the action following protocol steps │
|
||||||
|
│ - Load required context per 02_CONTEXT_LOADER.md │
|
||||||
|
│ - Follow protocol step-by-step │
|
||||||
|
│ - Handle errors with OP_06 patterns │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 4. VERIFY │
|
||||||
|
│ Confirm success │
|
||||||
|
│ - Files created correctly? │
|
||||||
|
│ - No errors in output? │
|
||||||
|
│ - Results make sense? │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 5. REPORT │
|
||||||
|
│ Summarize what was done │
|
||||||
|
│ - List files created/modified │
|
||||||
|
│ - Show key results │
|
||||||
|
│ - Note any warnings │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 6. SUGGEST │
|
||||||
|
│ Offer logical next steps │
|
||||||
|
│ - What should user do next? │
|
||||||
|
│ - Related operations available? │
|
||||||
|
│ - Dashboard URL if relevant? │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Classification Rules
|
||||||
|
|
||||||
|
Before executing, classify the user's request:
|
||||||
|
|
||||||
|
### Step 1: Identify Task Category
|
||||||
|
|
||||||
|
```python
|
||||||
|
TASK_CATEGORIES = {
|
||||||
|
"CREATE": {
|
||||||
|
"keywords": ["new", "create", "set up", "optimize", "study", "build"],
|
||||||
|
"protocol": "OP_01_CREATE_STUDY",
|
||||||
|
"privilege": "user"
|
||||||
|
},
|
||||||
|
"RUN": {
|
||||||
|
"keywords": ["start", "run", "execute", "begin", "launch"],
|
||||||
|
"protocol": "OP_02_RUN_OPTIMIZATION",
|
||||||
|
"privilege": "user"
|
||||||
|
},
|
||||||
|
"MONITOR": {
|
||||||
|
"keywords": ["status", "progress", "check", "how many", "trials"],
|
||||||
|
"protocol": "OP_03_MONITOR_PROGRESS",
|
||||||
|
"privilege": "user"
|
||||||
|
},
|
||||||
|
"ANALYZE": {
|
||||||
|
"keywords": ["results", "best", "compare", "pareto", "report"],
|
||||||
|
"protocol": "OP_04_ANALYZE_RESULTS",
|
||||||
|
"privilege": "user"
|
||||||
|
},
|
||||||
|
"EXPORT": {
|
||||||
|
"keywords": ["export", "training data", "neural data"],
|
||||||
|
"protocol": "OP_05_EXPORT_TRAINING_DATA",
|
||||||
|
"privilege": "user"
|
||||||
|
},
|
||||||
|
"DEBUG": {
|
||||||
|
"keywords": ["error", "failed", "not working", "crashed", "help"],
|
||||||
|
"protocol": "OP_06_TROUBLESHOOT",
|
||||||
|
"privilege": "user"
|
||||||
|
},
|
||||||
|
"EXTEND": {
|
||||||
|
"keywords": ["add extractor", "create hook", "new protocol"],
|
||||||
|
"protocol": "EXT_*",
|
||||||
|
"privilege": "power_user+"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Check Privilege
|
||||||
|
|
||||||
|
```python
|
||||||
|
def check_privilege(task_category, user_role):
|
||||||
|
required = TASK_CATEGORIES[task_category]["privilege"]
|
||||||
|
|
||||||
|
privilege_hierarchy = ["user", "power_user", "admin"]
|
||||||
|
|
||||||
|
if privilege_hierarchy.index(user_role) >= privilege_hierarchy.index(required):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Inform user they need higher privilege
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Load Context
|
||||||
|
|
||||||
|
Follow rules in `02_CONTEXT_LOADER.md` to load appropriate documentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Checkpoints
|
||||||
|
|
||||||
|
Before executing any protocol step, validate:
|
||||||
|
|
||||||
|
### Pre-Study Creation
|
||||||
|
- [ ] Model files exist (`.prt`, `.sim`)
|
||||||
|
- [ ] Working directory is writable
|
||||||
|
- [ ] User has described objectives clearly
|
||||||
|
- [ ] Conda environment is atomizer
|
||||||
|
|
||||||
|
### Pre-Run
|
||||||
|
- [ ] `optimization_config.json` exists and is valid
|
||||||
|
- [ ] `run_optimization.py` exists
|
||||||
|
- [ ] Model files copied to `1_setup/model/`
|
||||||
|
- [ ] No conflicting process running
|
||||||
|
|
||||||
|
### Pre-Analysis
|
||||||
|
- [ ] `study.db` exists with completed trials
|
||||||
|
- [ ] No optimization currently running
|
||||||
|
|
||||||
|
### Pre-Extension (power_user+)
|
||||||
|
- [ ] User has confirmed their role
|
||||||
|
- [ ] Extension doesn't duplicate existing functionality
|
||||||
|
- [ ] Tests can be written for new code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Recovery Protocol
|
||||||
|
|
||||||
|
When something fails during execution:
|
||||||
|
|
||||||
|
### Step 1: Identify Failure Point
|
||||||
|
```
|
||||||
|
Which step failed?
|
||||||
|
├─ File creation? → Check permissions, disk space
|
||||||
|
├─ NX solve? → Check NX log, timeout, expressions
|
||||||
|
├─ Extraction? → Check OP2 exists, subcase correct
|
||||||
|
├─ Database? → Check SQLite file, trial count
|
||||||
|
└─ Unknown? → Capture full error, check OP_06
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Attempt Recovery
|
||||||
|
|
||||||
|
```python
|
||||||
|
RECOVERY_ACTIONS = {
|
||||||
|
"file_permission": "Check directory permissions, try different location",
|
||||||
|
"nx_timeout": "Increase timeout in config, simplify model",
|
||||||
|
"nx_expression_error": "Verify expression names match NX model",
|
||||||
|
"op2_missing": "Check NX solve completed successfully",
|
||||||
|
"extractor_error": "Verify correct subcase and element types",
|
||||||
|
"database_locked": "Wait for other process to finish, or kill stale process",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Escalate if Needed
|
||||||
|
|
||||||
|
If recovery fails:
|
||||||
|
1. Log the error with full context
|
||||||
|
2. Inform user of the issue
|
||||||
|
3. Suggest manual intervention if appropriate
|
||||||
|
4. Offer to retry after user fixes underlying issue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Combination Rules
|
||||||
|
|
||||||
|
Some protocols work together, others conflict:
|
||||||
|
|
||||||
|
### Valid Combinations
|
||||||
|
```
|
||||||
|
OP_01 + SYS_10 # Create study with IMSO
|
||||||
|
OP_01 + SYS_11 # Create multi-objective study
|
||||||
|
OP_01 + SYS_14 # Create study with neural acceleration
|
||||||
|
OP_02 + SYS_13 # Run with dashboard tracking
|
||||||
|
OP_04 + SYS_11 # Analyze multi-objective results
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid Combinations
|
||||||
|
```
|
||||||
|
SYS_10 + SYS_11 # Single-obj IMSO with multi-obj NSGA (pick one)
|
||||||
|
TPESampler + SYS_11 # TPE is single-objective; use NSGAIISampler
|
||||||
|
EXT_* without privilege # Extensions require power_user or admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Protocol Inference
|
||||||
|
```
|
||||||
|
If objectives.length == 1:
|
||||||
|
→ Use Protocol 10 (single-objective)
|
||||||
|
→ Sampler: TPE, CMA-ES, or GP
|
||||||
|
|
||||||
|
If objectives.length > 1:
|
||||||
|
→ Use Protocol 11 (multi-objective)
|
||||||
|
→ Sampler: NSGA-II (mandatory)
|
||||||
|
|
||||||
|
If n_trials > 50 OR surrogate_settings present:
|
||||||
|
→ Add Protocol 14 (neural acceleration)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Logging
|
||||||
|
|
||||||
|
During execution, maintain awareness of:
|
||||||
|
|
||||||
|
### Session State
|
||||||
|
```python
|
||||||
|
session_state = {
|
||||||
|
"current_study": None, # Active study name
|
||||||
|
"loaded_protocols": [], # Protocols currently loaded
|
||||||
|
"completed_steps": [], # Steps completed this session
|
||||||
|
"pending_actions": [], # Actions waiting for user
|
||||||
|
"last_error": None, # Most recent error if any
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Communication
|
||||||
|
- Always explain what you're doing
|
||||||
|
- Show progress for long operations
|
||||||
|
- Warn before destructive actions
|
||||||
|
- Confirm before expensive operations (many trials)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Confirmation Requirements
|
||||||
|
|
||||||
|
Some actions require explicit user confirmation:
|
||||||
|
|
||||||
|
### Always Confirm
|
||||||
|
- [ ] Deleting files or studies
|
||||||
|
- [ ] Overwriting existing study
|
||||||
|
- [ ] Running >100 trials
|
||||||
|
- [ ] Modifying master NX files (FORBIDDEN - but confirm user understands)
|
||||||
|
- [ ] Creating extension (power_user+)
|
||||||
|
|
||||||
|
### Confirm If Uncertain
|
||||||
|
- [ ] Ambiguous objective (minimize or maximize?)
|
||||||
|
- [ ] Multiple possible extractors
|
||||||
|
- [ ] Complex multi-solution setup
|
||||||
|
|
||||||
|
### No Confirmation Needed
|
||||||
|
- [ ] Creating new study in empty directory
|
||||||
|
- [ ] Running validation checks
|
||||||
|
- [ ] Reading/analyzing results
|
||||||
|
- [ ] Checking status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format Standards
|
||||||
|
|
||||||
|
When reporting results:
|
||||||
|
|
||||||
|
### Study Creation Output
|
||||||
|
```
|
||||||
|
Created study: {study_name}
|
||||||
|
|
||||||
|
Files generated:
|
||||||
|
- studies/{study_name}/1_setup/optimization_config.json
|
||||||
|
- studies/{study_name}/run_optimization.py
|
||||||
|
- studies/{study_name}/README.md
|
||||||
|
- studies/{study_name}/STUDY_REPORT.md
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
- Design variables: {count}
|
||||||
|
- Objectives: {list}
|
||||||
|
- Constraints: {list}
|
||||||
|
- Protocol: {protocol}
|
||||||
|
- Trials: {n_trials}
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
1. Copy your NX files to studies/{study_name}/1_setup/model/
|
||||||
|
2. Run: conda activate atomizer && python run_optimization.py
|
||||||
|
3. Monitor: http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Status Output
|
||||||
|
```
|
||||||
|
Study: {study_name}
|
||||||
|
Status: {running|completed|failed}
|
||||||
|
Trials: {completed}/{total}
|
||||||
|
Best value: {value} ({objective_name})
|
||||||
|
Elapsed: {time}
|
||||||
|
|
||||||
|
Dashboard: http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Output
|
||||||
|
```
|
||||||
|
Error: {error_type}
|
||||||
|
Message: {error_message}
|
||||||
|
Location: {file}:{line}
|
||||||
|
|
||||||
|
Diagnosis:
|
||||||
|
{explanation}
|
||||||
|
|
||||||
|
Recovery:
|
||||||
|
{steps to fix}
|
||||||
|
|
||||||
|
Reference: OP_06_TROUBLESHOOT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quality Checklist
|
||||||
|
|
||||||
|
Before considering any task complete:
|
||||||
|
|
||||||
|
### For Study Creation
|
||||||
|
- [ ] `optimization_config.json` validates successfully
|
||||||
|
- [ ] `run_optimization.py` has no syntax errors
|
||||||
|
- [ ] `README.md` has all 11 required sections
|
||||||
|
- [ ] `STUDY_REPORT.md` template created
|
||||||
|
- [ ] No code duplication (used extractors from library)
|
||||||
|
|
||||||
|
### For Execution
|
||||||
|
- [ ] Optimization started without errors
|
||||||
|
- [ ] Dashboard shows real-time updates (if enabled)
|
||||||
|
- [ ] Trials are progressing
|
||||||
|
|
||||||
|
### For Analysis
|
||||||
|
- [ ] Best result(s) identified
|
||||||
|
- [ ] Constraints satisfied
|
||||||
|
- [ ] Report generated if requested
|
||||||
|
|
||||||
|
### For Extensions
|
||||||
|
- [ ] New code added to correct location
|
||||||
|
- [ ] `__init__.py` updated with exports
|
||||||
|
- [ ] Documentation updated
|
||||||
|
- [ ] Tests written (or noted as TODO)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Analyze Model Skill
|
# Analyze Model Skill
|
||||||
|
|
||||||
**Last Updated**: November 25, 2025
|
**Last Updated**: December 6, 2025
|
||||||
**Version**: 1.0 - Model Analysis and Feature Extraction
|
**Version**: 2.0 - Added Comprehensive Model Introspection
|
||||||
|
|
||||||
You are helping the user understand their NX model's structure and identify optimization opportunities.
|
You are helping the user understand their NX model's structure and identify optimization opportunities.
|
||||||
|
|
||||||
@@ -11,7 +11,8 @@ Extract and present information about an NX model to help the user:
|
|||||||
1. Identify available parametric expressions (potential design variables)
|
1. Identify available parametric expressions (potential design variables)
|
||||||
2. Understand the simulation setup (analysis types, boundary conditions)
|
2. Understand the simulation setup (analysis types, boundary conditions)
|
||||||
3. Discover material properties
|
3. Discover material properties
|
||||||
4. Recommend optimization strategies based on model characteristics
|
4. Identify extractable results from OP2 files
|
||||||
|
5. Recommend optimization strategies based on model characteristics
|
||||||
|
|
||||||
## Triggers
|
## Triggers
|
||||||
|
|
||||||
@@ -20,28 +21,107 @@ Extract and present information about an NX model to help the user:
|
|||||||
- "show me the expressions"
|
- "show me the expressions"
|
||||||
- "look at my NX model"
|
- "look at my NX model"
|
||||||
- "what parameters are available"
|
- "what parameters are available"
|
||||||
|
- "introspect my model"
|
||||||
|
- "what results are available"
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- User must provide path to NX model files (.prt, .sim, .fem)
|
- User must provide path to NX model files (.prt, .sim, .fem) or study directory
|
||||||
- NX must be available on the system (configured in config.py)
|
- NX must be available on the system for part/sim introspection
|
||||||
- Model files must be valid NX format
|
- OP2 introspection works without NX (pure Python)
|
||||||
|
|
||||||
## Information Gathering
|
## Information Gathering
|
||||||
|
|
||||||
Ask these questions if not already provided:
|
Ask these questions if not already provided:
|
||||||
|
|
||||||
1. **Model Location**:
|
1. **Model Location**:
|
||||||
- "Where is your NX model? (path to .prt file)"
|
- "Where is your NX model? (path to .prt file or study directory)"
|
||||||
- Default: Look in `studies/*/1_setup/model/`
|
- Default: Look in `studies/*/1_setup/model/`
|
||||||
|
|
||||||
2. **Analysis Interest**:
|
2. **Analysis Interest**:
|
||||||
- "What type of optimization are you considering?" (optional)
|
- "What type of optimization are you considering?" (optional)
|
||||||
- This helps focus the analysis on relevant aspects
|
- This helps focus the analysis on relevant aspects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MANDATORY: Model Introspection
|
||||||
|
|
||||||
|
**ALWAYS use the introspection module for comprehensive model analysis:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.hooks.nx_cad.model_introspection import (
|
||||||
|
introspect_part,
|
||||||
|
introspect_simulation,
|
||||||
|
introspect_op2,
|
||||||
|
introspect_study
|
||||||
|
)
|
||||||
|
|
||||||
|
# Option 1: Introspect entire study directory (recommended)
|
||||||
|
study_info = introspect_study("studies/my_study/")
|
||||||
|
|
||||||
|
# Option 2: Introspect individual files
|
||||||
|
part_info = introspect_part("path/to/model.prt")
|
||||||
|
sim_info = introspect_simulation("path/to/model.sim")
|
||||||
|
op2_info = introspect_op2("path/to/results.op2")
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Introspection Extracts
|
||||||
|
|
||||||
|
| Source | Information Extracted |
|
||||||
|
|--------|----------------------|
|
||||||
|
| `.prt` | Expressions (count, values, types), bodies, mass, material, features |
|
||||||
|
| `.sim` | Solutions, boundary conditions, loads, materials, mesh info, output requests |
|
||||||
|
| `.op2` | Available results (displacement, stress, strain, SPC forces, etc.), subcases |
|
||||||
|
|
||||||
|
### Introspection Report Generation
|
||||||
|
|
||||||
|
**MANDATORY**: Generate `MODEL_INTROSPECTION.md` for every study:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Generate and save introspection report
|
||||||
|
study_info = introspect_study(study_dir)
|
||||||
|
|
||||||
|
# Create markdown report
|
||||||
|
report = generate_introspection_report(study_info)
|
||||||
|
with open(study_dir / "MODEL_INTROSPECTION.md", "w") as f:
|
||||||
|
f.write(report)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Execution Steps
|
## Execution Steps
|
||||||
|
|
||||||
### Step 1: Validate Model Files
|
### Step 1: Run Comprehensive Introspection
|
||||||
|
|
||||||
|
**Use the introspection module (MANDATORY)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.hooks.nx_cad.model_introspection import introspect_study
|
||||||
|
|
||||||
|
# Introspect the entire study
|
||||||
|
result = introspect_study("studies/my_study/")
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
# Part information
|
||||||
|
for part in result["data"]["parts"]:
|
||||||
|
print(f"Part: {part['file']}")
|
||||||
|
print(f" Expressions: {part['data'].get('expression_count', 0)}")
|
||||||
|
print(f" Bodies: {part['data'].get('body_count', 0)}")
|
||||||
|
|
||||||
|
# Simulation information
|
||||||
|
for sim in result["data"]["simulations"]:
|
||||||
|
print(f"Simulation: {sim['file']}")
|
||||||
|
print(f" Solutions: {sim['data'].get('solution_count', 0)}")
|
||||||
|
|
||||||
|
# OP2 results
|
||||||
|
for op2 in result["data"]["results"]:
|
||||||
|
print(f"OP2: {op2['file']}")
|
||||||
|
available = op2['data'].get('available_results', {})
|
||||||
|
print(f" Displacement: {available.get('displacement', False)}")
|
||||||
|
print(f" Stress: {available.get('stress', False)}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Validate Model Files
|
||||||
|
|
||||||
Check that required files exist:
|
Check that required files exist:
|
||||||
|
|
||||||
@@ -95,26 +175,21 @@ def validate_model_files(model_path: Path) -> dict:
|
|||||||
return result
|
return result
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Extract Expressions
|
### Step 3: Extract Expressions (via Introspection)
|
||||||
|
|
||||||
Use NX Python API to extract all parametric expressions:
|
The introspection module extracts expressions automatically:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# This requires running a journal inside NX
|
from optimization_engine.hooks.nx_cad.model_introspection import introspect_part
|
||||||
# Use the expression extractor from optimization_engine
|
|
||||||
|
|
||||||
from optimization_engine.extractors.expression_extractor import extract_all_expressions
|
result = introspect_part("path/to/model.prt")
|
||||||
|
if result["success"]:
|
||||||
expressions = extract_all_expressions(prt_file)
|
expressions = result["data"].get("expressions", [])
|
||||||
# Returns: [{'name': 'thickness', 'value': 2.0, 'unit': 'mm', 'formula': None}, ...]
|
for expr in expressions:
|
||||||
|
print(f" {expr['name']}: {expr['value']} {expr.get('unit', '')}")
|
||||||
```
|
```
|
||||||
|
|
||||||
**Manual Extraction Method** (if NX API not available):
|
### Step 4: Classify Expressions
|
||||||
1. Read the .prt file header for expression metadata
|
|
||||||
2. Look for common parameter naming patterns
|
|
||||||
3. Ask user to provide expression names from NX
|
|
||||||
|
|
||||||
### Step 3: Classify Expressions
|
|
||||||
|
|
||||||
Categorize expressions by likely purpose:
|
Categorize expressions by likely purpose:
|
||||||
|
|
||||||
@@ -154,53 +229,121 @@ Based on analysis, recommend:
|
|||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
Present analysis in structured format:
|
Present analysis using the **MODEL_INTROSPECTION.md** format:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Model Introspection Report
|
||||||
|
|
||||||
|
**Study**: {study_name}
|
||||||
|
**Generated**: {date}
|
||||||
|
**Introspection Version**: 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Files Discovered
|
||||||
|
|
||||||
|
| Type | File | Status |
|
||||||
|
|------|------|--------|
|
||||||
|
| Part (.prt) | {prt_file} | ✓ Found |
|
||||||
|
| Simulation (.sim) | {sim_file} | ✓ Found |
|
||||||
|
| FEM (.fem) | {fem_file} | ✓ Found |
|
||||||
|
| Results (.op2) | {op2_file} | ✓ Found |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Part Information
|
||||||
|
|
||||||
|
### Expressions (Potential Design Variables)
|
||||||
|
|
||||||
|
| Name | Value | Unit | Type | Optimization Candidate |
|
||||||
|
|------|-------|------|------|------------------------|
|
||||||
|
| thickness | 2.0 | mm | User | ✓ High |
|
||||||
|
| hole_diameter | 10.0 | mm | User | ✓ High |
|
||||||
|
| p173_mass | 0.125 | kg | Reference | Read-only |
|
||||||
|
|
||||||
|
### Mass Properties
|
||||||
|
|
||||||
|
| Property | Value | Unit |
|
||||||
|
|----------|-------|------|
|
||||||
|
| Mass | 0.125 | kg |
|
||||||
|
| Material | Aluminum 6061-T6 | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Simulation Information
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
| Solution | Type | Nastran SOL | Status |
|
||||||
|
|----------|------|-------------|--------|
|
||||||
|
| Solution 1 | Static | SOL 101 | ✓ Active |
|
||||||
|
| Solution 2 | Modal | SOL 103 | ✓ Active |
|
||||||
|
|
||||||
|
### Boundary Conditions
|
||||||
|
|
||||||
|
| Name | Type | Applied To |
|
||||||
|
|------|------|------------|
|
||||||
|
| Fixed_Root | SPC | Face_1 |
|
||||||
|
|
||||||
|
### Loads
|
||||||
|
|
||||||
|
| Name | Type | Magnitude | Direction |
|
||||||
|
|------|------|-----------|-----------|
|
||||||
|
| Tip_Force | FORCE | 500 N | -Z |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Available Results (from OP2)
|
||||||
|
|
||||||
|
| Result Type | Available | Subcases |
|
||||||
|
|-------------|-----------|----------|
|
||||||
|
| Displacement | ✓ | 1 |
|
||||||
|
| SPC Forces | ✓ | 1 |
|
||||||
|
| Stress (CHEXA) | ✓ | 1 |
|
||||||
|
| Stress (CPENTA) | ✓ | 1 |
|
||||||
|
| Strain Energy | ✗ | - |
|
||||||
|
| Frequencies | ✓ | 2 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Optimization Recommendations
|
||||||
|
|
||||||
|
### Suggested Objectives
|
||||||
|
|
||||||
|
| Objective | Extractor | Source |
|
||||||
|
|-----------|-----------|--------|
|
||||||
|
| Minimize mass | E4: `extract_mass_from_bdf` | .dat |
|
||||||
|
| Maximize stiffness | E1: `extract_displacement` → k=F/δ | .op2 |
|
||||||
|
|
||||||
|
### Suggested Constraints
|
||||||
|
|
||||||
|
| Constraint | Type | Threshold | Extractor |
|
||||||
|
|------------|------|-----------|-----------|
|
||||||
|
| Max stress | less_than | 250 MPa | E3: `extract_solid_stress` |
|
||||||
|
|
||||||
|
### Recommended Protocol
|
||||||
|
|
||||||
|
- **Protocol 11 (Multi-Objective NSGA-II)** - Multiple competing objectives
|
||||||
|
- Multi-Solution: **Yes** (static + modal)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Ready to create optimization study? Say "create study" to proceed.*
|
||||||
```
|
```
|
||||||
MODEL ANALYSIS REPORT
|
|
||||||
=====================
|
|
||||||
|
|
||||||
Model: {model_name}
|
### Saving the Report
|
||||||
Location: {model_path}
|
|
||||||
|
|
||||||
FILES FOUND
|
**MANDATORY**: Save the introspection report to the study directory:
|
||||||
-----------
|
|
||||||
✓ Part file: {prt_file}
|
|
||||||
✓ Simulation: {sim_file}
|
|
||||||
✓ FEM mesh: {fem_file}
|
|
||||||
|
|
||||||
PARAMETRIC EXPRESSIONS
|
```python
|
||||||
----------------------
|
from pathlib import Path
|
||||||
| Name | Current Value | Unit | Category | Optimization Candidate |
|
|
||||||
|------|---------------|------|----------|----------------------|
|
|
||||||
| thickness | 2.0 | mm | Structural | ✓ High |
|
|
||||||
| hole_diameter | 10.0 | mm | Geometric | ✓ High |
|
|
||||||
| fillet_radius | 3.0 | mm | Geometric | ✓ Medium |
|
|
||||||
| length | 100.0 | mm | Dimensional | ? Check constraints |
|
|
||||||
|
|
||||||
SIMULATION SETUP
|
def save_introspection_report(study_dir: Path, report_content: str):
|
||||||
----------------
|
"""Save MODEL_INTROSPECTION.md to study directory."""
|
||||||
Analysis Types: Static (SOL 101), Modal (SOL 103)
|
report_path = study_dir / "MODEL_INTROSPECTION.md"
|
||||||
Material: Aluminum 6061-T6 (E=68.9 GPa, ρ=2700 kg/m³)
|
with open(report_path, 'w') as f:
|
||||||
Loads:
|
f.write(report_content)
|
||||||
- Force: 500 N at tip
|
print(f"Saved introspection report: {report_path}")
|
||||||
- Constraint: Fixed at root
|
|
||||||
|
|
||||||
RECOMMENDATIONS
|
|
||||||
---------------
|
|
||||||
Suggested Objectives:
|
|
||||||
- Minimize mass (extract from p173 expression or FEM)
|
|
||||||
- Maximize first natural frequency
|
|
||||||
|
|
||||||
Suggested Constraints:
|
|
||||||
- Max von Mises stress < 276 MPa (Al 6061 yield)
|
|
||||||
- Max displacement < {user to specify}
|
|
||||||
|
|
||||||
Recommended Protocol: Protocol 11 (Multi-Objective NSGA-II)
|
|
||||||
- Reason: Multiple competing objectives (mass vs frequency)
|
|
||||||
|
|
||||||
Ready to create optimization study? Say "create study" to proceed.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
|
|||||||
738
.claude/skills/core/study-creation-core.md
Normal file
738
.claude/skills/core/study-creation-core.md
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
# Study Creation Core Skill
|
||||||
|
|
||||||
|
**Last Updated**: December 6, 2025
|
||||||
|
**Version**: 2.3 - Added Model Introspection
|
||||||
|
**Type**: Core Skill
|
||||||
|
|
||||||
|
You are helping the user create a complete Atomizer optimization study from a natural language description.
|
||||||
|
|
||||||
|
**CRITICAL**: This skill is your SINGLE SOURCE OF TRUTH. DO NOT improvise or look at other studies for patterns. Use ONLY the patterns documented here and in the loaded modules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module Loading
|
||||||
|
|
||||||
|
This core skill is always loaded. Additional modules are loaded based on context:
|
||||||
|
|
||||||
|
| Module | Load When | Path |
|
||||||
|
|--------|-----------|------|
|
||||||
|
| **extractors-catalog** | Always (for reference) | `modules/extractors-catalog.md` |
|
||||||
|
| **zernike-optimization** | "telescope", "mirror", "optical", "wavefront" | `modules/zernike-optimization.md` |
|
||||||
|
| **neural-acceleration** | >50 trials, "neural", "surrogate", "fast" | `modules/neural-acceleration.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MANDATORY: Model Introspection at Study Creation
|
||||||
|
|
||||||
|
**ALWAYS run introspection when creating a study or when user asks:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.hooks.nx_cad.model_introspection import (
|
||||||
|
introspect_part,
|
||||||
|
introspect_simulation,
|
||||||
|
introspect_op2,
|
||||||
|
introspect_study
|
||||||
|
)
|
||||||
|
|
||||||
|
# Introspect entire study directory (recommended)
|
||||||
|
study_info = introspect_study("studies/my_study/")
|
||||||
|
|
||||||
|
# Or introspect individual files
|
||||||
|
part_info = introspect_part("path/to/model.prt")
|
||||||
|
sim_info = introspect_simulation("path/to/model.sim")
|
||||||
|
op2_info = introspect_op2("path/to/results.op2")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Introspection Extracts
|
||||||
|
|
||||||
|
| Source | Information |
|
||||||
|
|--------|-------------|
|
||||||
|
| `.prt` | Expressions (count, values, types), bodies, mass, material, features |
|
||||||
|
| `.sim` | Solutions, boundary conditions, loads, materials, mesh info, output requests |
|
||||||
|
| `.op2` | Available results (displacement, stress, strain, SPC forces, etc.), subcases |
|
||||||
|
|
||||||
|
### Generate Introspection Report
|
||||||
|
|
||||||
|
**MANDATORY**: Save `MODEL_INTROSPECTION.md` to study directory at creation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# After introspection, generate and save report
|
||||||
|
study_info = introspect_study(study_dir)
|
||||||
|
# Generate markdown report and save to studies/{study_name}/MODEL_INTROSPECTION.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MANDATORY DOCUMENTATION CHECKLIST
|
||||||
|
|
||||||
|
**EVERY study MUST have these files. A study is NOT complete without them:**
|
||||||
|
|
||||||
|
| File | Purpose | When Created |
|
||||||
|
|------|---------|--------------|
|
||||||
|
| `MODEL_INTROSPECTION.md` | **Model Analysis** - Expressions, solutions, available results | At study creation |
|
||||||
|
| `README.md` | **Engineering Blueprint** - Full mathematical formulation | At study creation |
|
||||||
|
| `STUDY_REPORT.md` | **Results Tracking** - Progress, best designs, recommendations | At study creation (template) |
|
||||||
|
|
||||||
|
**README.md Requirements (11 sections)**:
|
||||||
|
1. Engineering Problem (objective, physical system)
|
||||||
|
2. Mathematical Formulation (objectives, design variables, constraints with LaTeX)
|
||||||
|
3. Optimization Algorithm (config, properties, return format)
|
||||||
|
4. Simulation Pipeline (trial execution flow diagram)
|
||||||
|
5. Result Extraction Methods (extractor details, code snippets)
|
||||||
|
6. Neural Acceleration (surrogate config, expected performance)
|
||||||
|
7. Study File Structure (directory tree)
|
||||||
|
8. Results Location (output files)
|
||||||
|
9. Quick Start (commands)
|
||||||
|
10. Configuration Reference (config.json mapping)
|
||||||
|
11. References
|
||||||
|
|
||||||
|
**FAILURE MODE**: If you create a study without MODEL_INTROSPECTION.md, README.md, and STUDY_REPORT.md, the study is incomplete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR.3 NXSolver Interface
|
||||||
|
|
||||||
|
**Module**: `optimization_engine.nx_solver`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.nx_solver import NXSolver
|
||||||
|
|
||||||
|
nx_solver = NXSolver(
|
||||||
|
nastran_version="2412", # NX version
|
||||||
|
timeout=600, # Max solve time (seconds)
|
||||||
|
use_journal=True, # Use journal mode (recommended)
|
||||||
|
enable_session_management=True,
|
||||||
|
study_name="my_study"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Main Method - `run_simulation()`**:
|
||||||
|
```python
|
||||||
|
result = nx_solver.run_simulation(
|
||||||
|
sim_file=sim_file, # Path to .sim file
|
||||||
|
working_dir=model_dir, # Working directory
|
||||||
|
expression_updates=design_vars, # Dict: {'param_name': value}
|
||||||
|
solution_name=None, # None = solve ALL solutions
|
||||||
|
cleanup=True # Remove temp files after
|
||||||
|
)
|
||||||
|
|
||||||
|
# Returns:
|
||||||
|
# {
|
||||||
|
# 'success': bool,
|
||||||
|
# 'op2_file': Path,
|
||||||
|
# 'log_file': Path,
|
||||||
|
# 'elapsed_time': float,
|
||||||
|
# 'errors': list,
|
||||||
|
# 'solution_name': str
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL**: For multi-solution workflows (static + modal), set `solution_name=None`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR.4 Sampler Configurations
|
||||||
|
|
||||||
|
| Sampler | Use Case | Import | Config |
|
||||||
|
|---------|----------|--------|--------|
|
||||||
|
| **NSGAIISampler** | Multi-objective (2-3 objectives) | `from optuna.samplers import NSGAIISampler` | `NSGAIISampler(population_size=20, mutation_prob=0.1, crossover_prob=0.9, seed=42)` |
|
||||||
|
| **TPESampler** | Single-objective | `from optuna.samplers import TPESampler` | `TPESampler(seed=42)` |
|
||||||
|
| **CmaEsSampler** | Single-objective, continuous | `from optuna.samplers import CmaEsSampler` | `CmaEsSampler(seed=42)` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR.5 Study Creation Patterns
|
||||||
|
|
||||||
|
**Multi-Objective (NSGA-II)**:
|
||||||
|
```python
|
||||||
|
study = optuna.create_study(
|
||||||
|
study_name=study_name,
|
||||||
|
storage=f"sqlite:///{results_dir / 'study.db'}",
|
||||||
|
sampler=NSGAIISampler(population_size=20, seed=42),
|
||||||
|
directions=['minimize', 'maximize'], # [obj1_dir, obj2_dir]
|
||||||
|
load_if_exists=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Single-Objective (TPE)**:
|
||||||
|
```python
|
||||||
|
study = optuna.create_study(
|
||||||
|
study_name=study_name,
|
||||||
|
storage=f"sqlite:///{results_dir / 'study.db'}",
|
||||||
|
sampler=TPESampler(seed=42),
|
||||||
|
direction='minimize', # or 'maximize'
|
||||||
|
load_if_exists=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR.6 Objective Function Return Formats
|
||||||
|
|
||||||
|
**Multi-Objective** (directions=['minimize', 'minimize']):
|
||||||
|
```python
|
||||||
|
def objective(trial) -> Tuple[float, float]:
|
||||||
|
# ... extraction ...
|
||||||
|
return (obj1, obj2) # Both positive, framework handles direction
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-Objective with maximize** (directions=['maximize', 'minimize']):
|
||||||
|
```python
|
||||||
|
def objective(trial) -> Tuple[float, float]:
|
||||||
|
# ... extraction ...
|
||||||
|
return (-stiffness, mass) # -stiffness so minimize → maximize
|
||||||
|
```
|
||||||
|
|
||||||
|
**Single-Objective**:
|
||||||
|
```python
|
||||||
|
def objective(trial) -> float:
|
||||||
|
# ... extraction ...
|
||||||
|
return objective_value
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR.7 Hook System
|
||||||
|
|
||||||
|
**Available Hook Points** (from `optimization_engine.plugins.hooks`):
|
||||||
|
|
||||||
|
| Hook Point | When | Context Keys |
|
||||||
|
|------------|------|--------------|
|
||||||
|
| `PRE_MESH` | Before meshing | `trial_number, design_variables, sim_file` |
|
||||||
|
| `POST_MESH` | After mesh | `trial_number, design_variables, sim_file` |
|
||||||
|
| `PRE_SOLVE` | Before solve | `trial_number, design_variables, sim_file, working_dir` |
|
||||||
|
| `POST_SOLVE` | After solve | `trial_number, design_variables, op2_file, working_dir` |
|
||||||
|
| `POST_EXTRACTION` | After extraction | `trial_number, design_variables, results, working_dir` |
|
||||||
|
| `POST_CALCULATION` | After calculations | `trial_number, objectives, constraints, feasible` |
|
||||||
|
| `CUSTOM_OBJECTIVE` | Custom objectives | `trial_number, design_variables, extracted_results` |
|
||||||
|
|
||||||
|
See [EXT_02_CREATE_HOOK](../../docs/protocols/extensions/EXT_02_CREATE_HOOK.md) for creating custom hooks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR.8 Structured Logging (MANDATORY)
|
||||||
|
|
||||||
|
**Always use structured logging**:
|
||||||
|
```python
|
||||||
|
from optimization_engine.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(study_name, study_dir=results_dir)
|
||||||
|
|
||||||
|
# Study lifecycle
|
||||||
|
logger.study_start(study_name, n_trials, "NSGAIISampler")
|
||||||
|
logger.study_complete(study_name, total_trials, successful_trials)
|
||||||
|
|
||||||
|
# Trial lifecycle
|
||||||
|
logger.trial_start(trial.number, design_vars)
|
||||||
|
logger.trial_complete(trial.number, objectives_dict, constraints_dict, feasible)
|
||||||
|
logger.trial_failed(trial.number, error_message)
|
||||||
|
|
||||||
|
# General logging
|
||||||
|
logger.info("message")
|
||||||
|
logger.warning("message")
|
||||||
|
logger.error("message", exc_info=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Study Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
studies/{study_name}/
|
||||||
|
├── 1_setup/ # INPUT: Configuration & Model
|
||||||
|
│ ├── model/ # WORKING COPY of NX Files
|
||||||
|
│ │ ├── {Model}.prt # Parametric part
|
||||||
|
│ │ ├── {Model}_sim1.sim # Simulation setup
|
||||||
|
│ │ └── *.dat, *.op2, *.f06 # Solver outputs
|
||||||
|
│ ├── optimization_config.json # Study configuration
|
||||||
|
│ └── workflow_config.json # Workflow metadata
|
||||||
|
├── 2_results/ # OUTPUT: Results
|
||||||
|
│ ├── study.db # Optuna SQLite database
|
||||||
|
│ └── optimization_history.json # Trial history
|
||||||
|
├── run_optimization.py # Main entry point
|
||||||
|
├── reset_study.py # Database reset
|
||||||
|
├── README.md # Engineering blueprint
|
||||||
|
└── STUDY_REPORT.md # Results report template
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRITICAL: Model File Protection
|
||||||
|
|
||||||
|
**NEVER modify the user's original/master model files.** Always work on copies.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def setup_working_copy(source_dir: Path, model_dir: Path, file_patterns: list):
|
||||||
|
"""Copy model files from user's source to study working directory."""
|
||||||
|
model_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for pattern in file_patterns:
|
||||||
|
for src_file in source_dir.glob(pattern):
|
||||||
|
dst_file = model_dir / src_file.name
|
||||||
|
if not dst_file.exists():
|
||||||
|
shutil.copy2(src_file, dst_file)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interactive Discovery Process
|
||||||
|
|
||||||
|
### Step 1: Problem Understanding
|
||||||
|
|
||||||
|
**Ask clarifying questions**:
|
||||||
|
- "What component are you optimizing?"
|
||||||
|
- "What do you want to optimize?" (minimize/maximize)
|
||||||
|
- "What limits must be satisfied?" (constraints)
|
||||||
|
- "What parameters can be changed?" (design variables)
|
||||||
|
- "Where are your NX files?"
|
||||||
|
|
||||||
|
### Step 2: Protocol Selection
|
||||||
|
|
||||||
|
| Scenario | Protocol | Sampler |
|
||||||
|
|----------|----------|---------|
|
||||||
|
| Single objective + constraints | Protocol 10 | TPE/CMA-ES |
|
||||||
|
| 2-3 objectives | Protocol 11 | NSGA-II |
|
||||||
|
| >50 trials, need speed | Protocol 14 | + Neural |
|
||||||
|
|
||||||
|
### Step 3: Extractor Mapping
|
||||||
|
|
||||||
|
Map user needs to extractors from [extractors-catalog module](../modules/extractors-catalog.md):
|
||||||
|
|
||||||
|
| Need | Extractor |
|
||||||
|
|------|-----------|
|
||||||
|
| Displacement | E1: `extract_displacement` |
|
||||||
|
| Stress | E3: `extract_solid_stress` |
|
||||||
|
| Frequency | E2: `extract_frequency` |
|
||||||
|
| Mass (FEM) | E4: `extract_mass_from_bdf` |
|
||||||
|
| Mass (CAD) | E5: `extract_mass_from_expression` |
|
||||||
|
|
||||||
|
### Step 4: Multi-Solution Detection
|
||||||
|
|
||||||
|
If user needs BOTH:
|
||||||
|
- Static results (stress, displacement)
|
||||||
|
- Modal results (frequency)
|
||||||
|
|
||||||
|
Then set `solution_name=None` to solve ALL solutions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Generation
|
||||||
|
|
||||||
|
### 1. optimization_config.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"study_name": "{study_name}",
|
||||||
|
"description": "{concise description}",
|
||||||
|
|
||||||
|
"optimization_settings": {
|
||||||
|
"protocol": "protocol_11_multi_objective",
|
||||||
|
"n_trials": 30,
|
||||||
|
"sampler": "NSGAIISampler",
|
||||||
|
"timeout_per_trial": 600
|
||||||
|
},
|
||||||
|
|
||||||
|
"design_variables": [
|
||||||
|
{
|
||||||
|
"parameter": "{nx_expression_name}",
|
||||||
|
"bounds": [min, max],
|
||||||
|
"description": "{what this controls}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"objectives": [
|
||||||
|
{
|
||||||
|
"name": "{objective_name}",
|
||||||
|
"goal": "minimize",
|
||||||
|
"weight": 1.0,
|
||||||
|
"description": "{what this measures}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"constraints": [
|
||||||
|
{
|
||||||
|
"name": "{constraint_name}",
|
||||||
|
"type": "less_than",
|
||||||
|
"threshold": value,
|
||||||
|
"description": "{engineering justification}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"simulation": {
|
||||||
|
"model_file": "{Model}.prt",
|
||||||
|
"sim_file": "{Model}_sim1.sim",
|
||||||
|
"solver": "nastran"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. run_optimization.py Template
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
{Study Name} Optimization
|
||||||
|
{Brief description}
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
project_root = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
import optuna
|
||||||
|
from optuna.samplers import NSGAIISampler # or TPESampler
|
||||||
|
|
||||||
|
from optimization_engine.nx_solver import NXSolver
|
||||||
|
from optimization_engine.logger import get_logger
|
||||||
|
|
||||||
|
# Import extractors - USE ONLY FROM extractors-catalog module
|
||||||
|
from optimization_engine.extractors.extract_displacement import extract_displacement
|
||||||
|
from optimization_engine.extractors.bdf_mass_extractor import extract_mass_from_bdf
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config_file: Path) -> dict:
|
||||||
|
with open(config_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def objective(trial: optuna.Trial, config: dict, nx_solver: NXSolver,
|
||||||
|
model_dir: Path, logger) -> Tuple[float, float]:
|
||||||
|
"""Multi-objective function. Returns (obj1, obj2)."""
|
||||||
|
|
||||||
|
# 1. Sample design variables
|
||||||
|
design_vars = {}
|
||||||
|
for var in config['design_variables']:
|
||||||
|
param_name = var['parameter']
|
||||||
|
bounds = var['bounds']
|
||||||
|
design_vars[param_name] = trial.suggest_float(param_name, bounds[0], bounds[1])
|
||||||
|
|
||||||
|
logger.trial_start(trial.number, design_vars)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 2. Run simulation
|
||||||
|
sim_file = model_dir / config['simulation']['sim_file']
|
||||||
|
result = nx_solver.run_simulation(
|
||||||
|
sim_file=sim_file,
|
||||||
|
working_dir=model_dir,
|
||||||
|
expression_updates=design_vars,
|
||||||
|
solution_name=None, # Solve ALL solutions
|
||||||
|
cleanup=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result['success']:
|
||||||
|
logger.trial_failed(trial.number, f"Simulation failed")
|
||||||
|
return (float('inf'), float('inf'))
|
||||||
|
|
||||||
|
op2_file = result['op2_file']
|
||||||
|
|
||||||
|
# 3. Extract results
|
||||||
|
disp_result = extract_displacement(op2_file, subcase=1)
|
||||||
|
max_displacement = disp_result['max_displacement']
|
||||||
|
|
||||||
|
dat_file = model_dir / config['simulation'].get('dat_file', 'model.dat')
|
||||||
|
mass_kg = extract_mass_from_bdf(str(dat_file))
|
||||||
|
|
||||||
|
# 4. Calculate objectives
|
||||||
|
applied_force = 1000.0 # N
|
||||||
|
stiffness = applied_force / max(abs(max_displacement), 1e-6)
|
||||||
|
|
||||||
|
# 5. Set trial attributes
|
||||||
|
trial.set_user_attr('stiffness', stiffness)
|
||||||
|
trial.set_user_attr('mass', mass_kg)
|
||||||
|
|
||||||
|
objectives = {'stiffness': stiffness, 'mass': mass_kg}
|
||||||
|
logger.trial_complete(trial.number, objectives, {}, True)
|
||||||
|
|
||||||
|
return (-stiffness, mass_kg) # Negate stiffness to maximize
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.trial_failed(trial.number, str(e))
|
||||||
|
return (float('inf'), float('inf'))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='{Study Name} Optimization')
|
||||||
|
|
||||||
|
stage_group = parser.add_mutually_exclusive_group()
|
||||||
|
stage_group.add_argument('--discover', action='store_true')
|
||||||
|
stage_group.add_argument('--validate', action='store_true')
|
||||||
|
stage_group.add_argument('--test', action='store_true')
|
||||||
|
stage_group.add_argument('--train', action='store_true')
|
||||||
|
stage_group.add_argument('--run', action='store_true')
|
||||||
|
|
||||||
|
parser.add_argument('--trials', type=int, default=100)
|
||||||
|
parser.add_argument('--resume', action='store_true')
|
||||||
|
parser.add_argument('--enable-nn', action='store_true')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
study_dir = Path(__file__).parent
|
||||||
|
config_path = study_dir / "1_setup" / "optimization_config.json"
|
||||||
|
model_dir = study_dir / "1_setup" / "model"
|
||||||
|
results_dir = study_dir / "2_results"
|
||||||
|
results_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
study_name = "{study_name}"
|
||||||
|
|
||||||
|
logger = get_logger(study_name, study_dir=results_dir)
|
||||||
|
config = load_config(config_path)
|
||||||
|
nx_solver = NXSolver()
|
||||||
|
|
||||||
|
storage = f"sqlite:///{results_dir / 'study.db'}"
|
||||||
|
sampler = NSGAIISampler(population_size=20, seed=42)
|
||||||
|
|
||||||
|
logger.study_start(study_name, args.trials, "NSGAIISampler")
|
||||||
|
|
||||||
|
if args.resume:
|
||||||
|
study = optuna.load_study(study_name=study_name, storage=storage, sampler=sampler)
|
||||||
|
else:
|
||||||
|
study = optuna.create_study(
|
||||||
|
study_name=study_name,
|
||||||
|
storage=storage,
|
||||||
|
sampler=sampler,
|
||||||
|
directions=['minimize', 'minimize'],
|
||||||
|
load_if_exists=True
|
||||||
|
)
|
||||||
|
|
||||||
|
study.optimize(
|
||||||
|
lambda trial: objective(trial, config, nx_solver, model_dir, logger),
|
||||||
|
n_trials=args.trials,
|
||||||
|
show_progress_bar=True
|
||||||
|
)
|
||||||
|
|
||||||
|
n_successful = len([t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE])
|
||||||
|
logger.study_complete(study_name, len(study.trials), n_successful)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. reset_study.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Reset {study_name} optimization study by deleting database."""
|
||||||
|
import optuna
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
study_dir = Path(__file__).parent
|
||||||
|
storage = f"sqlite:///{study_dir / '2_results' / 'study.db'}"
|
||||||
|
study_name = "{study_name}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
optuna.delete_study(study_name=study_name, storage=storage)
|
||||||
|
print(f"[OK] Deleted study: {study_name}")
|
||||||
|
except KeyError:
|
||||||
|
print(f"[WARNING] Study '{study_name}' not found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Error: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Mass Minimization with Constraints
|
||||||
|
|
||||||
|
```
|
||||||
|
Objective: Minimize mass
|
||||||
|
Constraints: Stress < limit, Displacement < limit
|
||||||
|
Protocol: Protocol 10 (single-objective TPE)
|
||||||
|
Extractors: E4/E5, E3, E1
|
||||||
|
Multi-Solution: No (static only)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Mass vs Stiffness Trade-off
|
||||||
|
|
||||||
|
```
|
||||||
|
Objectives: Minimize mass, Maximize stiffness
|
||||||
|
Constraints: Stress < limit
|
||||||
|
Protocol: Protocol 11 (multi-objective NSGA-II)
|
||||||
|
Extractors: E4/E5, E1 (for stiffness = F/δ), E3
|
||||||
|
Multi-Solution: No (static only)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Mass vs Frequency Trade-off
|
||||||
|
|
||||||
|
```
|
||||||
|
Objectives: Minimize mass, Maximize frequency
|
||||||
|
Constraints: Stress < limit, Displacement < limit
|
||||||
|
Protocol: Protocol 11 (multi-objective NSGA-II)
|
||||||
|
Extractors: E4/E5, E2, E3, E1
|
||||||
|
Multi-Solution: Yes (static + modal)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Integration
|
||||||
|
|
||||||
|
### Pre-Flight Check
|
||||||
|
|
||||||
|
```python
|
||||||
|
def preflight_check():
|
||||||
|
"""Validate study setup before running."""
|
||||||
|
from optimization_engine.validators import validate_study
|
||||||
|
|
||||||
|
result = validate_study(STUDY_NAME)
|
||||||
|
|
||||||
|
if not result.is_ready_to_run:
|
||||||
|
print("[X] Study validation failed!")
|
||||||
|
print(result)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("[OK] Pre-flight check passed!")
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation Checklist
|
||||||
|
|
||||||
|
- [ ] All design variables have valid bounds (min < max)
|
||||||
|
- [ ] All objectives have proper extraction methods
|
||||||
|
- [ ] All constraints have thresholds defined
|
||||||
|
- [ ] Protocol matches objective count
|
||||||
|
- [ ] Part file (.prt) exists in model directory
|
||||||
|
- [ ] Simulation file (.sim) exists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
After completing study creation, provide:
|
||||||
|
|
||||||
|
**Summary Table**:
|
||||||
|
```
|
||||||
|
Study Created: {study_name}
|
||||||
|
Protocol: {protocol}
|
||||||
|
Objectives: {list}
|
||||||
|
Constraints: {list}
|
||||||
|
Design Variables: {list}
|
||||||
|
Multi-Solution: {Yes/No}
|
||||||
|
```
|
||||||
|
|
||||||
|
**File Checklist**:
|
||||||
|
```
|
||||||
|
✓ studies/{study_name}/1_setup/optimization_config.json
|
||||||
|
✓ studies/{study_name}/1_setup/workflow_config.json
|
||||||
|
✓ studies/{study_name}/run_optimization.py
|
||||||
|
✓ studies/{study_name}/reset_study.py
|
||||||
|
✓ studies/{study_name}/MODEL_INTROSPECTION.md # MANDATORY - Model analysis
|
||||||
|
✓ studies/{study_name}/README.md
|
||||||
|
✓ studies/{study_name}/STUDY_REPORT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Next Steps**:
|
||||||
|
```
|
||||||
|
1. Place your NX files in studies/{study_name}/1_setup/model/
|
||||||
|
2. Test with: python run_optimization.py --test
|
||||||
|
3. Monitor: http://localhost:3003
|
||||||
|
4. Full run: python run_optimization.py --run --trials {n_trials}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Reminders
|
||||||
|
|
||||||
|
1. **Multi-Objective Return Format**: Return tuple with positive values, use `directions` for semantics
|
||||||
|
2. **Multi-Solution**: Set `solution_name=None` for static + modal workflows
|
||||||
|
3. **Always use centralized extractors** from `optimization_engine/extractors/`
|
||||||
|
4. **Never modify master model files** - always work on copies
|
||||||
|
5. **Structured logging is mandatory** - use `get_logger()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assembly FEM (AFEM) Workflow
|
||||||
|
|
||||||
|
For complex assemblies with `.afm` files, the update sequence is critical:
|
||||||
|
|
||||||
|
```
|
||||||
|
.prt (geometry) → _fem1.fem (component mesh) → .afm (assembly mesh) → .sim (solution)
|
||||||
|
```
|
||||||
|
|
||||||
|
### The 4-Step Update Process
|
||||||
|
|
||||||
|
1. **Update Expressions in Geometry (.prt)**
|
||||||
|
- Open part, update expressions, DoUpdate(), Save
|
||||||
|
|
||||||
|
2. **Update ALL Linked Geometry Parts** (CRITICAL!)
|
||||||
|
- Open each linked part, DoUpdate(), Save
|
||||||
|
- **Skipping this causes corrupt results ("billion nm" RMS)**
|
||||||
|
|
||||||
|
3. **Update Component FEMs (.fem)**
|
||||||
|
- UpdateFemodel() regenerates mesh from updated geometry
|
||||||
|
|
||||||
|
4. **Update Assembly FEM (.afm)**
|
||||||
|
- UpdateFemodel(), merge coincident nodes at interfaces
|
||||||
|
|
||||||
|
### Assembly Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nx_settings": {
|
||||||
|
"expression_part": "M1_Blank",
|
||||||
|
"component_fems": ["M1_Blank_fem1.fem", "M1_Support_fem1.fem"],
|
||||||
|
"afm_file": "ASSY_M1_assyfem1.afm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-Solution Solve Protocol
|
||||||
|
|
||||||
|
When simulation has multiple solutions (static + modal), use `SolveAllSolutions` API:
|
||||||
|
|
||||||
|
### Critical: Foreground Mode Required
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG - Returns immediately, async
|
||||||
|
theCAESimSolveManager.SolveChainOfSolutions(
|
||||||
|
psolutions1,
|
||||||
|
SolveMode.Background # Returns before complete!
|
||||||
|
)
|
||||||
|
|
||||||
|
# CORRECT - Waits for completion
|
||||||
|
theCAESimSolveManager.SolveAllSolutions(
|
||||||
|
SolveOption.Solve,
|
||||||
|
SetupCheckOption.CompleteCheckAndOutputErrors,
|
||||||
|
SolveMode.Foreground, # Blocks until complete
|
||||||
|
False
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
- `solution_name=None` passed to `NXSolver.run_simulation()`
|
||||||
|
- Multiple solutions that must all complete
|
||||||
|
- Multi-objective requiring results from different analysis types
|
||||||
|
|
||||||
|
### Solution Monitor Control
|
||||||
|
|
||||||
|
Solution monitor is automatically disabled when solving multiple solutions to prevent window pile-up:
|
||||||
|
|
||||||
|
```python
|
||||||
|
propertyTable.SetBooleanPropertyValue("solution monitor", False)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
After solve, verify:
|
||||||
|
- Both `.dat` files written (one per solution)
|
||||||
|
- Both `.op2` files created with updated timestamps
|
||||||
|
- Results are unique per trial (frequency values vary)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Operations Protocol**: [OP_01_CREATE_STUDY](../../docs/protocols/operations/OP_01_CREATE_STUDY.md)
|
||||||
|
- **Extractors Module**: [extractors-catalog](../modules/extractors-catalog.md)
|
||||||
|
- **Zernike Module**: [zernike-optimization](../modules/zernike-optimization.md)
|
||||||
|
- **Neural Module**: [neural-acceleration](../modules/neural-acceleration.md)
|
||||||
|
- **System Protocols**: [SYS_10_IMSO](../../docs/protocols/system/SYS_10_IMSO.md), [SYS_11_MULTI_OBJECTIVE](../../docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md)
|
||||||
402
.claude/skills/create-study-wizard.md
Normal file
402
.claude/skills/create-study-wizard.md
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
# Create Study Wizard Skill
|
||||||
|
|
||||||
|
**Version**: 3.0 - StudyWizard Integration
|
||||||
|
**Last Updated**: 2025-12-06
|
||||||
|
|
||||||
|
You are helping the user create a complete Atomizer optimization study using the powerful `StudyWizard` class.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.study_wizard import StudyWizard, create_study, list_extractors
|
||||||
|
|
||||||
|
# Option 1: One-liner for simple studies
|
||||||
|
create_study(
|
||||||
|
study_name="my_study",
|
||||||
|
description="Optimize bracket for stiffness",
|
||||||
|
prt_file="path/to/model.prt",
|
||||||
|
design_variables=[
|
||||||
|
{"parameter": "thickness", "bounds": [5, 20], "units": "mm"}
|
||||||
|
],
|
||||||
|
objectives=[
|
||||||
|
{"name": "stiffness", "goal": "maximize", "extractor": "extract_displacement"}
|
||||||
|
],
|
||||||
|
constraints=[
|
||||||
|
{"name": "mass", "type": "less_than", "threshold": 0.5, "extractor": "extract_mass_from_bdf", "units": "kg"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Option 2: Step-by-step with full control
|
||||||
|
wizard = StudyWizard("my_study", "Optimize bracket")
|
||||||
|
wizard.set_model_files("path/to/model.prt")
|
||||||
|
wizard.introspect() # Discover expressions, solutions
|
||||||
|
wizard.add_design_variable("thickness", bounds=(5, 20), units="mm")
|
||||||
|
wizard.add_objective("mass", goal="minimize", extractor="extract_mass_from_bdf")
|
||||||
|
wizard.add_constraint("stress", type="less_than", threshold=250, extractor="extract_solid_stress", units="MPa")
|
||||||
|
wizard.generate()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trigger Phrases
|
||||||
|
|
||||||
|
Use this skill when user says:
|
||||||
|
- "create study", "new study", "set up study", "create optimization"
|
||||||
|
- "optimize my [part/model/bracket/component]"
|
||||||
|
- "help me minimize [mass/weight/cost]"
|
||||||
|
- "help me maximize [stiffness/strength/frequency]"
|
||||||
|
- "I want to find the best [design/parameters]"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Steps
|
||||||
|
|
||||||
|
### Step 1: Gather Requirements
|
||||||
|
|
||||||
|
Ask the user (if not already provided):
|
||||||
|
|
||||||
|
1. **Model files**: "Where is your NX model? (path to .prt file)"
|
||||||
|
2. **Optimization goal**: "What do you want to optimize?"
|
||||||
|
- Minimize mass/weight
|
||||||
|
- Maximize stiffness
|
||||||
|
- Target a specific frequency
|
||||||
|
- Multi-objective trade-off
|
||||||
|
3. **Constraints**: "What limits must be respected?"
|
||||||
|
- Max stress < yield/safety factor
|
||||||
|
- Max displacement < tolerance
|
||||||
|
- Mass budget
|
||||||
|
|
||||||
|
### Step 2: Introspect Model
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.study_wizard import StudyWizard
|
||||||
|
|
||||||
|
wizard = StudyWizard("study_name", "Description")
|
||||||
|
wizard.set_model_files("path/to/model.prt")
|
||||||
|
result = wizard.introspect()
|
||||||
|
|
||||||
|
# Show user what was found
|
||||||
|
print(f"Found {len(result.expressions)} expressions:")
|
||||||
|
for expr in result.expressions[:10]:
|
||||||
|
print(f" {expr['name']}: {expr.get('value', 'N/A')}")
|
||||||
|
|
||||||
|
print(f"\nFound {len(result.solutions)} solutions:")
|
||||||
|
for sol in result.solutions:
|
||||||
|
print(f" {sol['name']}")
|
||||||
|
|
||||||
|
# Suggest design variables
|
||||||
|
suggestions = result.suggest_design_variables()
|
||||||
|
for s in suggestions:
|
||||||
|
print(f" {s['name']}: {s['current_value']} -> bounds {s['suggested_bounds']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Configure Study
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add design variables from introspection suggestions
|
||||||
|
for dv in selected_design_variables:
|
||||||
|
wizard.add_design_variable(
|
||||||
|
parameter=dv['name'],
|
||||||
|
bounds=dv['bounds'],
|
||||||
|
units=dv.get('units', ''),
|
||||||
|
description=dv.get('description', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add objectives
|
||||||
|
wizard.add_objective(
|
||||||
|
name="mass",
|
||||||
|
goal="minimize",
|
||||||
|
extractor="extract_mass_from_bdf",
|
||||||
|
description="Minimize total bracket mass"
|
||||||
|
)
|
||||||
|
|
||||||
|
wizard.add_objective(
|
||||||
|
name="stiffness",
|
||||||
|
goal="maximize",
|
||||||
|
extractor="extract_displacement",
|
||||||
|
params={"invert_for_stiffness": True},
|
||||||
|
description="Maximize structural stiffness"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add constraints
|
||||||
|
wizard.add_constraint(
|
||||||
|
name="max_stress",
|
||||||
|
constraint_type="less_than",
|
||||||
|
threshold=250,
|
||||||
|
extractor="extract_solid_stress",
|
||||||
|
units="MPa",
|
||||||
|
description="Keep stress below yield/4"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set protocol based on objectives
|
||||||
|
if len(wizard.objectives) > 1:
|
||||||
|
wizard.set_protocol("protocol_11_multi") # NSGA-II
|
||||||
|
else:
|
||||||
|
wizard.set_protocol("protocol_10_single") # TPE
|
||||||
|
|
||||||
|
wizard.set_trials(100)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Generate Study
|
||||||
|
|
||||||
|
```python
|
||||||
|
files = wizard.generate()
|
||||||
|
|
||||||
|
print("Study generated successfully!")
|
||||||
|
print(f"Location: {wizard.study_dir}")
|
||||||
|
print("\nNext steps:")
|
||||||
|
print(" 1. cd", wizard.study_dir)
|
||||||
|
print(" 2. python run_optimization.py --discover")
|
||||||
|
print(" 3. python run_optimization.py --validate")
|
||||||
|
print(" 4. python run_optimization.py --run --trials 100")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Available Extractors
|
||||||
|
|
||||||
|
| Extractor | What it extracts | Input | Output |
|
||||||
|
|-----------|------------------|-------|--------|
|
||||||
|
| `extract_mass_from_bdf` | Total mass | .dat/.bdf | kg |
|
||||||
|
| `extract_part_mass` | CAD mass | .prt | kg |
|
||||||
|
| `extract_displacement` | Max displacement | .op2 | mm |
|
||||||
|
| `extract_solid_stress` | Von Mises stress | .op2 | MPa |
|
||||||
|
| `extract_principal_stress` | Principal stresses | .op2 | MPa |
|
||||||
|
| `extract_strain_energy` | Strain energy | .op2 | J |
|
||||||
|
| `extract_spc_forces` | Reaction forces | .op2 | N |
|
||||||
|
| `extract_frequency` | Natural frequencies | .op2 | Hz |
|
||||||
|
| `get_first_frequency` | First mode frequency | .f06 | Hz |
|
||||||
|
| `extract_temperature` | Nodal temperatures | .op2 | K/°C |
|
||||||
|
| `extract_modal_mass` | Modal effective mass | .f06 | kg |
|
||||||
|
| `extract_zernike_from_op2` | Zernike WFE | .op2+.bdf | nm |
|
||||||
|
|
||||||
|
**List all extractors programmatically**:
|
||||||
|
```python
|
||||||
|
from optimization_engine.study_wizard import list_extractors
|
||||||
|
for name, info in list_extractors().items():
|
||||||
|
print(f"{name}: {info['description']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Optimization Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Minimize Mass with Stress Constraint
|
||||||
|
|
||||||
|
```python
|
||||||
|
create_study(
|
||||||
|
study_name="lightweight_bracket",
|
||||||
|
description="Minimize mass while keeping stress below yield",
|
||||||
|
prt_file="Bracket.prt",
|
||||||
|
design_variables=[
|
||||||
|
{"parameter": "wall_thickness", "bounds": [2, 10], "units": "mm"},
|
||||||
|
{"parameter": "rib_count", "bounds": [2, 8], "units": "count"}
|
||||||
|
],
|
||||||
|
objectives=[
|
||||||
|
{"name": "mass", "goal": "minimize", "extractor": "extract_mass_from_bdf"}
|
||||||
|
],
|
||||||
|
constraints=[
|
||||||
|
{"name": "stress", "type": "less_than", "threshold": 250,
|
||||||
|
"extractor": "extract_solid_stress", "units": "MPa"}
|
||||||
|
],
|
||||||
|
protocol="protocol_10_single"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Multi-Objective Stiffness vs Mass
|
||||||
|
|
||||||
|
```python
|
||||||
|
create_study(
|
||||||
|
study_name="pareto_bracket",
|
||||||
|
description="Trade-off between stiffness and mass",
|
||||||
|
prt_file="Bracket.prt",
|
||||||
|
design_variables=[
|
||||||
|
{"parameter": "thickness", "bounds": [5, 25], "units": "mm"},
|
||||||
|
{"parameter": "support_angle", "bounds": [20, 70], "units": "degrees"}
|
||||||
|
],
|
||||||
|
objectives=[
|
||||||
|
{"name": "stiffness", "goal": "maximize", "extractor": "extract_displacement"},
|
||||||
|
{"name": "mass", "goal": "minimize", "extractor": "extract_mass_from_bdf"}
|
||||||
|
],
|
||||||
|
constraints=[
|
||||||
|
{"name": "mass_limit", "type": "less_than", "threshold": 0.5,
|
||||||
|
"extractor": "extract_mass_from_bdf", "units": "kg"}
|
||||||
|
],
|
||||||
|
protocol="protocol_11_multi",
|
||||||
|
n_trials=150
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Frequency-Targeted Modal Optimization
|
||||||
|
|
||||||
|
```python
|
||||||
|
create_study(
|
||||||
|
study_name="modal_bracket",
|
||||||
|
description="Tune first natural frequency to target",
|
||||||
|
prt_file="Bracket.prt",
|
||||||
|
design_variables=[
|
||||||
|
{"parameter": "thickness", "bounds": [3, 15], "units": "mm"},
|
||||||
|
{"parameter": "length", "bounds": [50, 150], "units": "mm"}
|
||||||
|
],
|
||||||
|
objectives=[
|
||||||
|
{"name": "frequency_error", "goal": "minimize",
|
||||||
|
"extractor": "get_first_frequency",
|
||||||
|
"params": {"target": 100}} # Target 100 Hz
|
||||||
|
],
|
||||||
|
constraints=[
|
||||||
|
{"name": "mass", "type": "less_than", "threshold": 0.3,
|
||||||
|
"extractor": "extract_mass_from_bdf", "units": "kg"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Thermal Optimization
|
||||||
|
|
||||||
|
```python
|
||||||
|
create_study(
|
||||||
|
study_name="heat_sink",
|
||||||
|
description="Minimize max temperature",
|
||||||
|
prt_file="HeatSink.prt",
|
||||||
|
design_variables=[
|
||||||
|
{"parameter": "fin_height", "bounds": [10, 50], "units": "mm"},
|
||||||
|
{"parameter": "fin_count", "bounds": [5, 20], "units": "count"}
|
||||||
|
],
|
||||||
|
objectives=[
|
||||||
|
{"name": "max_temp", "goal": "minimize", "extractor": "get_max_temperature"}
|
||||||
|
],
|
||||||
|
constraints=[
|
||||||
|
{"name": "mass", "type": "less_than", "threshold": 0.2,
|
||||||
|
"extractor": "extract_mass_from_bdf", "units": "kg"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Selection Guide
|
||||||
|
|
||||||
|
| Scenario | Protocol | Sampler |
|
||||||
|
|----------|----------|---------|
|
||||||
|
| Single objective | `protocol_10_single` | TPESampler |
|
||||||
|
| Multiple objectives (Pareto) | `protocol_11_multi` | NSGAIISampler |
|
||||||
|
| Smooth design space | `protocol_10_single` | CmaEsSampler |
|
||||||
|
| Discrete variables | `protocol_10_single` | TPESampler |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Generated
|
||||||
|
|
||||||
|
The wizard generates a complete study structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
studies/{study_name}/
|
||||||
|
├── 1_setup/
|
||||||
|
│ ├── model/ # NX model files (copied)
|
||||||
|
│ ├── optimization_config.json
|
||||||
|
│ └── workflow_config.json
|
||||||
|
├── 2_results/ # Created on first run
|
||||||
|
├── run_optimization.py # Main script with staged workflow
|
||||||
|
├── reset_study.py # Reset results
|
||||||
|
├── README.md # Engineering documentation
|
||||||
|
├── STUDY_REPORT.md # Results tracking template
|
||||||
|
└── MODEL_INTROSPECTION.md # Model analysis report
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Staged Workflow
|
||||||
|
|
||||||
|
After generation, guide user through staged validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stage 1: Discover model outputs
|
||||||
|
python run_optimization.py --discover
|
||||||
|
|
||||||
|
# Stage 2: Validate with single trial
|
||||||
|
python run_optimization.py --validate
|
||||||
|
|
||||||
|
# Stage 3: Integration test (3 trials)
|
||||||
|
python run_optimization.py --test
|
||||||
|
|
||||||
|
# Stage 4: Full optimization
|
||||||
|
python run_optimization.py --run --trials 100
|
||||||
|
|
||||||
|
# Resume if interrupted
|
||||||
|
python run_optimization.py --run --trials 50 --resume
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Issues and Solutions
|
||||||
|
|
||||||
|
| Error | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| "Part file not found" | Wrong path | Use absolute path or relative to studies/ |
|
||||||
|
| "No expressions found" | Part not parametric | Add expressions in NX Modeling |
|
||||||
|
| "Simulation failed" | NX not running | Start NX before running |
|
||||||
|
| "Extractor not found" | Typo in name | Use `list_extractors()` to see valid names |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Conversation
|
||||||
|
|
||||||
|
**User**: "I want to optimize my bracket for minimum weight while keeping stress low"
|
||||||
|
|
||||||
|
**Claude**: "I'll help you create a weight optimization study. Let me gather some information:
|
||||||
|
|
||||||
|
1. Where is your NX model file?
|
||||||
|
2. What's the maximum allowable stress (e.g., yield/4)?
|
||||||
|
3. Are there specific parameters you want to vary (thickness, angles, etc.)?"
|
||||||
|
|
||||||
|
**User**: "The model is at studies/my_bracket/model/Bracket.prt. Max stress should be 250 MPa. I want to vary the wall_thickness and support_angle."
|
||||||
|
|
||||||
|
**Claude**: "I'll create the study and introspect your model to confirm the parameters exist."
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.study_wizard import StudyWizard
|
||||||
|
|
||||||
|
wizard = StudyWizard(
|
||||||
|
study_name="my_bracket_optimization",
|
||||||
|
description="Minimize bracket mass with stress constraint"
|
||||||
|
)
|
||||||
|
|
||||||
|
wizard.set_model_files("studies/my_bracket/model/Bracket.prt")
|
||||||
|
|
||||||
|
# Introspect to verify parameters
|
||||||
|
result = wizard.introspect()
|
||||||
|
print(f"Found expressions: {result.get_expression_names()}")
|
||||||
|
|
||||||
|
# Configure
|
||||||
|
wizard.add_design_variable("wall_thickness", bounds=(2, 15), units="mm")
|
||||||
|
wizard.add_design_variable("support_angle", bounds=(20, 60), units="degrees")
|
||||||
|
wizard.add_objective("mass", goal="minimize", extractor="extract_mass_from_bdf")
|
||||||
|
wizard.add_constraint("stress", constraint_type="less_than", threshold=250,
|
||||||
|
extractor="extract_solid_stress", units="MPa")
|
||||||
|
wizard.set_trials(100)
|
||||||
|
|
||||||
|
files = wizard.generate()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Claude**: "Study created! Here's what I generated:
|
||||||
|
- `studies/my_bracket_optimization/run_optimization.py`
|
||||||
|
- `studies/my_bracket_optimization/README.md`
|
||||||
|
- Configuration files in `1_setup/`
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
1. `cd studies/my_bracket_optimization`
|
||||||
|
2. `python run_optimization.py --discover`
|
||||||
|
3. `python run_optimization.py --validate`
|
||||||
|
4. `python run_optimization.py --run --trials 100`"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Extractor Library**: `docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md`
|
||||||
|
- **Protocol 10 (IMSO)**: `docs/protocols/system/SYS_10_IMSO.md`
|
||||||
|
- **Protocol 11 (Multi-Objective)**: `docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md`
|
||||||
|
- **StudyWizard Source**: `optimization_engine/study_wizard.py`
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Create Optimization Study Skill
|
# Create Optimization Study Skill
|
||||||
|
|
||||||
**Last Updated**: December 4, 2025
|
**Last Updated**: December 6, 2025
|
||||||
**Version**: 2.1 - Added Mandatory Documentation Requirements
|
**Version**: 2.2 - Added Model Introspection
|
||||||
|
|
||||||
You are helping the user create a complete Atomizer optimization study from a natural language description.
|
You are helping the user create a complete Atomizer optimization study from a natural language description.
|
||||||
|
|
||||||
@@ -9,12 +9,50 @@ You are helping the user create a complete Atomizer optimization study from a na
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## MANDATORY: Model Introspection
|
||||||
|
|
||||||
|
**ALWAYS run introspection when user provides NX files or asks for model analysis:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.hooks.nx_cad.model_introspection import (
|
||||||
|
introspect_part,
|
||||||
|
introspect_simulation,
|
||||||
|
introspect_op2,
|
||||||
|
introspect_study
|
||||||
|
)
|
||||||
|
|
||||||
|
# Introspect entire study directory (recommended)
|
||||||
|
study_info = introspect_study("studies/my_study/")
|
||||||
|
|
||||||
|
# Or introspect individual files
|
||||||
|
part_info = introspect_part("path/to/model.prt")
|
||||||
|
sim_info = introspect_simulation("path/to/model.sim")
|
||||||
|
op2_info = introspect_op2("path/to/results.op2")
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Introspection Provides
|
||||||
|
|
||||||
|
| Source | Information Extracted |
|
||||||
|
|--------|----------------------|
|
||||||
|
| `.prt` | Expressions (potential design variables), bodies, mass, material, features |
|
||||||
|
| `.sim` | Solutions (SOL types), boundary conditions, loads, materials, mesh info, output requests |
|
||||||
|
| `.op2` | Available results (displacement, stress, strain, SPC forces, frequencies), subcases |
|
||||||
|
|
||||||
|
### Generate MODEL_INTROSPECTION.md
|
||||||
|
|
||||||
|
**MANDATORY**: Save introspection report at study creation:
|
||||||
|
- Location: `studies/{study_name}/MODEL_INTROSPECTION.md`
|
||||||
|
- Contains: All expressions, solutions, available results, optimization recommendations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## MANDATORY DOCUMENTATION CHECKLIST
|
## MANDATORY DOCUMENTATION CHECKLIST
|
||||||
|
|
||||||
**EVERY study MUST have these files. A study is NOT complete without them:**
|
**EVERY study MUST have these files. A study is NOT complete without them:**
|
||||||
|
|
||||||
| File | Purpose | When Created |
|
| File | Purpose | When Created |
|
||||||
|------|---------|--------------|
|
|------|---------|--------------|
|
||||||
|
| `MODEL_INTROSPECTION.md` | **Model Analysis** - Expressions, solutions, available results | At study creation |
|
||||||
| `README.md` | **Engineering Blueprint** - Full mathematical formulation, design variables, objectives, algorithm config | At study creation |
|
| `README.md` | **Engineering Blueprint** - Full mathematical formulation, design variables, objectives, algorithm config | At study creation |
|
||||||
| `STUDY_REPORT.md` | **Results Tracking** - Progress, best designs, surrogate accuracy, recommendations | At study creation (template) |
|
| `STUDY_REPORT.md` | **Results Tracking** - Progress, best designs, surrogate accuracy, recommendations | At study creation (template) |
|
||||||
|
|
||||||
@@ -2053,6 +2091,7 @@ Multi-Solution: {Yes/No}
|
|||||||
✓ studies/{study_name}/1_setup/workflow_config.json
|
✓ studies/{study_name}/1_setup/workflow_config.json
|
||||||
✓ studies/{study_name}/run_optimization.py
|
✓ studies/{study_name}/run_optimization.py
|
||||||
✓ studies/{study_name}/reset_study.py
|
✓ studies/{study_name}/reset_study.py
|
||||||
|
✓ studies/{study_name}/MODEL_INTROSPECTION.md # MANDATORY - Model analysis
|
||||||
✓ studies/{study_name}/README.md # Engineering blueprint
|
✓ studies/{study_name}/README.md # Engineering blueprint
|
||||||
✓ studies/{study_name}/STUDY_REPORT.md # MANDATORY - Results report template
|
✓ studies/{study_name}/STUDY_REPORT.md # MANDATORY - Results report template
|
||||||
[✓] studies/{study_name}/NX_FILE_MODIFICATIONS_REQUIRED.md (if needed)
|
[✓] studies/{study_name}/NX_FILE_MODIFICATIONS_REQUIRED.md (if needed)
|
||||||
|
|||||||
325
.claude/skills/guided-study-wizard.md
Normal file
325
.claude/skills/guided-study-wizard.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# Guided Study Creation Wizard
|
||||||
|
|
||||||
|
**Version**: 1.0
|
||||||
|
**Purpose**: Interactive conversational wizard for creating new optimization studies from scratch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill provides a step-by-step guided experience for users who want to create a new optimization study. It asks focused questions to gather requirements, then generates the complete study configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wizard Flow
|
||||||
|
|
||||||
|
### Phase 1: Understanding the Problem (Discovery)
|
||||||
|
|
||||||
|
Start with open-ended questions to understand what the user wants to optimize:
|
||||||
|
|
||||||
|
**Opening Prompt:**
|
||||||
|
```
|
||||||
|
I'll help you set up a new optimization study. Let's start with the basics:
|
||||||
|
|
||||||
|
1. **What are you trying to optimize?**
|
||||||
|
- Describe the physical system (e.g., "a telescope mirror", "a UAV arm", "a bracket")
|
||||||
|
|
||||||
|
2. **What's your goal?**
|
||||||
|
- Minimize weight? Maximize stiffness? Minimize stress? Multiple objectives?
|
||||||
|
|
||||||
|
3. **Do you have an NX model ready?**
|
||||||
|
- If yes, where is it located?
|
||||||
|
- If no, we can discuss what's needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Model Analysis (If NX model provided)
|
||||||
|
|
||||||
|
If user provides a model path:
|
||||||
|
|
||||||
|
1. **Check the model exists**
|
||||||
|
```python
|
||||||
|
# Verify path
|
||||||
|
model_path = Path(user_provided_path)
|
||||||
|
if model_path.exists():
|
||||||
|
# Proceed with analysis
|
||||||
|
else:
|
||||||
|
# Ask for correct path
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Extract expressions (design parameters)**
|
||||||
|
- List all NX expressions that could be design variables
|
||||||
|
- Ask user to confirm which ones to optimize
|
||||||
|
|
||||||
|
3. **Identify simulation setup**
|
||||||
|
- What solution types are present? (static, modal, buckling)
|
||||||
|
- What results are available?
|
||||||
|
|
||||||
|
### Phase 3: Define Objectives & Constraints
|
||||||
|
|
||||||
|
Ask focused questions:
|
||||||
|
|
||||||
|
```
|
||||||
|
Based on your model, I can see these results are available:
|
||||||
|
- Displacement (from static solution)
|
||||||
|
- Von Mises stress (from static solution)
|
||||||
|
- Natural frequency (from modal solution)
|
||||||
|
- Mass (from geometry)
|
||||||
|
|
||||||
|
**Questions:**
|
||||||
|
|
||||||
|
1. **Primary Objective** - What do you want to minimize/maximize?
|
||||||
|
Examples: "minimize tip displacement", "minimize mass"
|
||||||
|
|
||||||
|
2. **Secondary Objectives** (optional) - Any other goals?
|
||||||
|
Examples: "also minimize stress", "maximize first frequency"
|
||||||
|
|
||||||
|
3. **Constraints** - What limits must be respected?
|
||||||
|
Examples: "stress < 200 MPa", "frequency > 50 Hz", "mass < 2 kg"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Define Design Space
|
||||||
|
|
||||||
|
For each design variable identified:
|
||||||
|
|
||||||
|
```
|
||||||
|
For parameter `{param_name}` (current value: {current_value}):
|
||||||
|
- **Minimum value**: (default: -20% of current)
|
||||||
|
- **Maximum value**: (default: +20% of current)
|
||||||
|
- **Type**: continuous or discrete?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5: Optimization Settings
|
||||||
|
|
||||||
|
```
|
||||||
|
**Optimization Configuration:**
|
||||||
|
|
||||||
|
1. **Number of trials**: How thorough should the search be?
|
||||||
|
- Quick exploration: 50-100 trials
|
||||||
|
- Standard: 100-200 trials
|
||||||
|
- Thorough: 200-500 trials
|
||||||
|
- With neural acceleration: 500+ trials
|
||||||
|
|
||||||
|
2. **Protocol Selection** (I'll recommend based on your setup):
|
||||||
|
- Single objective → Protocol 10 (IMSO)
|
||||||
|
- Multi-objective (2-3 goals) → Protocol 11 (NSGA-II)
|
||||||
|
- Large-scale with NN → Protocol 12 (Hybrid)
|
||||||
|
|
||||||
|
3. **Neural Network Acceleration**:
|
||||||
|
- Enable if n_trials > 100 and you want faster iterations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 6: Summary & Confirmation
|
||||||
|
|
||||||
|
Present the complete configuration for user approval:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Study Configuration Summary
|
||||||
|
|
||||||
|
**Study Name**: {study_name}
|
||||||
|
**Location**: studies/{study_name}/
|
||||||
|
|
||||||
|
**Model**: {model_path}
|
||||||
|
|
||||||
|
**Design Variables** ({n_vars} parameters):
|
||||||
|
| Parameter | Min | Max | Type |
|
||||||
|
|-----------|-----|-----|------|
|
||||||
|
| {name1} | {min1} | {max1} | continuous |
|
||||||
|
| ... | ... | ... | ... |
|
||||||
|
|
||||||
|
**Objectives**:
|
||||||
|
- {objective1}: {direction1}
|
||||||
|
- {objective2}: {direction2} (if multi-objective)
|
||||||
|
|
||||||
|
**Constraints**:
|
||||||
|
- {constraint1}
|
||||||
|
- {constraint2}
|
||||||
|
|
||||||
|
**Settings**:
|
||||||
|
- Protocol: {protocol}
|
||||||
|
- Trials: {n_trials}
|
||||||
|
- Sampler: {sampler}
|
||||||
|
- Neural Acceleration: {enabled/disabled}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Does this look correct?
|
||||||
|
- Type "yes" to generate the study files
|
||||||
|
- Type "change X" to modify a specific setting
|
||||||
|
- Type "start over" to begin again
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 7: Generation
|
||||||
|
|
||||||
|
Once confirmed, generate:
|
||||||
|
|
||||||
|
1. Create study directory structure
|
||||||
|
2. Copy model files to working directory
|
||||||
|
3. Generate `optimization_config.json`
|
||||||
|
4. Generate `run_optimization.py`
|
||||||
|
5. Validate everything works
|
||||||
|
|
||||||
|
```
|
||||||
|
✓ Study created successfully!
|
||||||
|
|
||||||
|
**Next Steps:**
|
||||||
|
1. Review the generated files in studies/{study_name}/
|
||||||
|
2. Run a quick validation: `python run_optimization.py --validate`
|
||||||
|
3. Start optimization: `python run_optimization.py --start`
|
||||||
|
|
||||||
|
Or just tell me "start the optimization" and I'll handle it!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Question Templates
|
||||||
|
|
||||||
|
### For Understanding Goals
|
||||||
|
|
||||||
|
- "What problem are you trying to solve?"
|
||||||
|
- "What makes a 'good' design for your application?"
|
||||||
|
- "Are there any hard limits that must not be exceeded?"
|
||||||
|
- "Is this a weight reduction study, a performance study, or both?"
|
||||||
|
|
||||||
|
### For Design Variables
|
||||||
|
|
||||||
|
- "Which dimensions or parameters should I vary?"
|
||||||
|
- "Are there any parameters that must stay fixed?"
|
||||||
|
- "What are reasonable bounds for {parameter}?"
|
||||||
|
- "Should {parameter} be continuous or discrete (specific values only)?"
|
||||||
|
|
||||||
|
### For Constraints
|
||||||
|
|
||||||
|
- "What's the maximum stress this component can handle?"
|
||||||
|
- "Is there a minimum stiffness requirement?"
|
||||||
|
- "Are there weight limits?"
|
||||||
|
- "What frequency should the structure avoid (resonance concerns)?"
|
||||||
|
|
||||||
|
### For Optimization Settings
|
||||||
|
|
||||||
|
- "How much time can you allocate to this study?"
|
||||||
|
- "Do you need a quick exploration or thorough optimization?"
|
||||||
|
- "Is this a preliminary study or final optimization?"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Default Configurations by Use Case
|
||||||
|
|
||||||
|
### Structural Weight Minimization
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"objectives": [
|
||||||
|
{"name": "mass", "direction": "minimize", "target": null}
|
||||||
|
],
|
||||||
|
"constraints": [
|
||||||
|
{"name": "max_stress", "type": "<=", "value": 200e6, "unit": "Pa"},
|
||||||
|
{"name": "max_displacement", "type": "<=", "value": 0.001, "unit": "m"}
|
||||||
|
],
|
||||||
|
"n_trials": 150,
|
||||||
|
"sampler": "TPE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Objective (Weight vs Performance)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"objectives": [
|
||||||
|
{"name": "mass", "direction": "minimize"},
|
||||||
|
{"name": "max_displacement", "direction": "minimize"}
|
||||||
|
],
|
||||||
|
"n_trials": 200,
|
||||||
|
"sampler": "NSGA-II"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modal Optimization (Frequency Tuning)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"objectives": [
|
||||||
|
{"name": "first_frequency", "direction": "maximize"}
|
||||||
|
],
|
||||||
|
"constraints": [
|
||||||
|
{"name": "mass", "type": "<=", "value": 5.0, "unit": "kg"}
|
||||||
|
],
|
||||||
|
"n_trials": 150,
|
||||||
|
"sampler": "TPE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Telescope Mirror (Zernike WFE)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"objectives": [
|
||||||
|
{"name": "filtered_rms", "direction": "minimize", "unit": "nm"}
|
||||||
|
],
|
||||||
|
"constraints": [
|
||||||
|
{"name": "mass", "type": "<=", "value": null}
|
||||||
|
],
|
||||||
|
"extractor": "ZernikeExtractor",
|
||||||
|
"n_trials": 200,
|
||||||
|
"sampler": "NSGA-II"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Model Not Found
|
||||||
|
```
|
||||||
|
I couldn't find a model at that path. Let's verify:
|
||||||
|
- Current directory: {cwd}
|
||||||
|
- You specified: {user_path}
|
||||||
|
|
||||||
|
Could you check the path and try again?
|
||||||
|
Tip: Use an absolute path like "C:/Users/.../model.prt"
|
||||||
|
```
|
||||||
|
|
||||||
|
### No Expressions Found
|
||||||
|
```
|
||||||
|
I couldn't find any parametric expressions in this model.
|
||||||
|
|
||||||
|
For optimization, we need parameters defined as NX expressions.
|
||||||
|
Would you like me to explain how to add expressions to your model?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid Constraint
|
||||||
|
```
|
||||||
|
That constraint doesn't match any available results.
|
||||||
|
|
||||||
|
Available results from your model:
|
||||||
|
- {result1}
|
||||||
|
- {result2}
|
||||||
|
|
||||||
|
Which of these would you like to constrain?
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Dashboard
|
||||||
|
|
||||||
|
When running from the Atomizer dashboard with a connected Claude terminal:
|
||||||
|
|
||||||
|
1. **No study selected** → Offer to create a new study
|
||||||
|
2. **Study selected** → Use that study's context, offer to modify or run
|
||||||
|
|
||||||
|
The dashboard will display the study once created, showing real-time progress.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Commands
|
||||||
|
|
||||||
|
For users who know what they want:
|
||||||
|
|
||||||
|
- `create study {name} from {model_path}` - Skip to model analysis
|
||||||
|
- `quick setup` - Use all defaults, just confirm
|
||||||
|
- `copy study {existing} as {new}` - Clone an existing study as starting point
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remember
|
||||||
|
|
||||||
|
- **Be conversational** - This is a wizard, not a form
|
||||||
|
- **Offer sensible defaults** - Don't make users specify everything
|
||||||
|
- **Validate as you go** - Catch issues early
|
||||||
|
- **Explain decisions** - Say why you recommend certain settings
|
||||||
|
- **Keep it focused** - One question at a time, don't overwhelm
|
||||||
289
.claude/skills/modules/extractors-catalog.md
Normal file
289
.claude/skills/modules/extractors-catalog.md
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
# Extractors Catalog Module
|
||||||
|
|
||||||
|
**Last Updated**: December 5, 2025
|
||||||
|
**Version**: 1.0
|
||||||
|
**Type**: Optional Module
|
||||||
|
|
||||||
|
This module documents all available extractors in the Atomizer framework. Load this when the user asks about result extraction or needs to understand what extractors are available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Load
|
||||||
|
|
||||||
|
- User asks "what extractors are available?"
|
||||||
|
- User needs to extract results from OP2/BDF files
|
||||||
|
- Setting up a new study with custom extraction needs
|
||||||
|
- Debugging extraction issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR.1 Extractor Catalog
|
||||||
|
|
||||||
|
| ID | Extractor | Module | Function | Input | Output | Returns |
|
||||||
|
|----|-----------|--------|----------|-------|--------|---------|
|
||||||
|
| E1 | **Displacement** | `optimization_engine.extractors.extract_displacement` | `extract_displacement(op2_file, subcase=1)` | `.op2` | mm | `{'max_displacement': float, 'max_disp_node': int, 'max_disp_x/y/z': float}` |
|
||||||
|
| E2 | **Frequency** | `optimization_engine.extractors.extract_frequency` | `extract_frequency(op2_file, subcase=1, mode_number=1)` | `.op2` | Hz | `{'frequency': float, 'mode_number': int, 'eigenvalue': float, 'all_frequencies': list}` |
|
||||||
|
| E3 | **Von Mises Stress** | `optimization_engine.extractors.extract_von_mises_stress` | `extract_solid_stress(op2_file, subcase=1, element_type='cquad4')` | `.op2` | MPa | `{'max_von_mises': float, 'max_stress_element': int}` |
|
||||||
|
| E4 | **BDF Mass** | `optimization_engine.extractors.bdf_mass_extractor` | `extract_mass_from_bdf(bdf_file)` | `.dat`/`.bdf` | kg | `float` (mass in kg) |
|
||||||
|
| E5 | **CAD Expression Mass** | `optimization_engine.extractors.extract_mass_from_expression` | `extract_mass_from_expression(prt_file, expression_name='p173')` | `.prt` + `_temp_mass.txt` | kg | `float` (mass in kg) |
|
||||||
|
| E6 | **Field Data** | `optimization_engine.extractors.field_data_extractor` | `FieldDataExtractor(field_file, result_column, aggregation)` | `.fld`/`.csv` | varies | `{'value': float, 'stats': dict}` |
|
||||||
|
| E7 | **Stiffness** | `optimization_engine.extractors.stiffness_calculator` | `StiffnessCalculator(field_file, op2_file, force_component, displacement_component)` | `.fld` + `.op2` | N/mm | `{'stiffness': float, 'displacement': float, 'force': float}` |
|
||||||
|
| E11 | **Part Mass & Material** | `optimization_engine.extractors.extract_part_mass_material` | `extract_part_mass_material(prt_file)` | `.prt` | kg + dict | `{'mass_kg': float, 'volume_mm3': float, 'material': {'name': str}, ...}` |
|
||||||
|
|
||||||
|
**For Zernike extractors (E8-E10)**, see the [zernike-optimization module](./zernike-optimization.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR.2 Extractor Code Snippets (COPY-PASTE)
|
||||||
|
|
||||||
|
### E1: Displacement Extraction
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors.extract_displacement import extract_displacement
|
||||||
|
|
||||||
|
disp_result = extract_displacement(op2_file, subcase=1)
|
||||||
|
max_displacement = disp_result['max_displacement'] # mm
|
||||||
|
max_node = disp_result['max_disp_node'] # Node ID
|
||||||
|
```
|
||||||
|
|
||||||
|
**Return Dictionary**:
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'max_displacement': 0.523, # Maximum magnitude (mm)
|
||||||
|
'max_disp_node': 1234, # Node ID with max displacement
|
||||||
|
'max_disp_x': 0.123, # X component at max node
|
||||||
|
'max_disp_y': 0.456, # Y component at max node
|
||||||
|
'max_disp_z': 0.234 # Z component at max node
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### E2: Frequency Extraction
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors.extract_frequency import extract_frequency
|
||||||
|
|
||||||
|
# Get first mode frequency
|
||||||
|
freq_result = extract_frequency(op2_file, subcase=1, mode_number=1)
|
||||||
|
frequency = freq_result['frequency'] # Hz
|
||||||
|
|
||||||
|
# Get all frequencies
|
||||||
|
all_freqs = freq_result['all_frequencies'] # List of all mode frequencies
|
||||||
|
```
|
||||||
|
|
||||||
|
**Return Dictionary**:
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'frequency': 125.4, # Requested mode frequency (Hz)
|
||||||
|
'mode_number': 1, # Mode number requested
|
||||||
|
'eigenvalue': 6.21e5, # Eigenvalue (rad/s)^2
|
||||||
|
'all_frequencies': [125.4, 234.5, 389.2, ...] # All mode frequencies
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### E3: Stress Extraction
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors.extract_von_mises_stress import extract_solid_stress
|
||||||
|
|
||||||
|
# For shell elements (CQUAD4, CTRIA3)
|
||||||
|
stress_result = extract_solid_stress(op2_file, subcase=1, element_type='cquad4')
|
||||||
|
|
||||||
|
# For solid elements (CTETRA, CHEXA)
|
||||||
|
stress_result = extract_solid_stress(op2_file, subcase=1, element_type='ctetra')
|
||||||
|
|
||||||
|
max_stress = stress_result['max_von_mises'] # MPa
|
||||||
|
```
|
||||||
|
|
||||||
|
**Return Dictionary**:
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'max_von_mises': 187.5, # Maximum von Mises stress (MPa)
|
||||||
|
'max_stress_element': 5678, # Element ID with max stress
|
||||||
|
'mean_stress': 45.2, # Mean stress across all elements
|
||||||
|
'stress_distribution': {...} # Optional: full distribution data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### E4: BDF Mass Extraction
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors.bdf_mass_extractor import extract_mass_from_bdf
|
||||||
|
|
||||||
|
mass_kg = extract_mass_from_bdf(str(dat_file)) # kg
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Calculates mass from element properties and material density in the BDF/DAT file.
|
||||||
|
|
||||||
|
### E5: CAD Expression Mass
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors.extract_mass_from_expression import extract_mass_from_expression
|
||||||
|
|
||||||
|
mass_kg = extract_mass_from_expression(model_file, expression_name="p173") # kg
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Requires `_temp_mass.txt` to be written by solve journal. The expression name is the NX expression that contains the mass value.
|
||||||
|
|
||||||
|
### E6: Field Data Extraction
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors.field_data_extractor import FieldDataExtractor
|
||||||
|
|
||||||
|
# Create extractor
|
||||||
|
extractor = FieldDataExtractor(
|
||||||
|
field_file="results.fld",
|
||||||
|
result_column="Temperature",
|
||||||
|
aggregation="max" # or "min", "mean", "sum"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = extractor.extract()
|
||||||
|
value = result['value'] # Aggregated value
|
||||||
|
stats = result['stats'] # Full statistics
|
||||||
|
```
|
||||||
|
|
||||||
|
### E7: Stiffness Calculation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Simple stiffness from displacement (most common)
|
||||||
|
applied_force = 1000.0 # N - MUST MATCH YOUR MODEL'S APPLIED LOAD
|
||||||
|
stiffness = applied_force / max(abs(max_displacement), 1e-6) # N/mm
|
||||||
|
|
||||||
|
# Or using StiffnessCalculator for complex cases
|
||||||
|
from optimization_engine.extractors.stiffness_calculator import StiffnessCalculator
|
||||||
|
|
||||||
|
calc = StiffnessCalculator(
|
||||||
|
field_file="displacement.fld",
|
||||||
|
op2_file="results.op2",
|
||||||
|
force_component="Fz",
|
||||||
|
displacement_component="Tz"
|
||||||
|
)
|
||||||
|
result = calc.calculate()
|
||||||
|
stiffness = result['stiffness'] # N/mm
|
||||||
|
```
|
||||||
|
|
||||||
|
### E11: Part Mass & Material Extraction
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors import extract_part_mass_material, extract_part_mass
|
||||||
|
|
||||||
|
# Full extraction with all properties
|
||||||
|
result = extract_part_mass_material(prt_file)
|
||||||
|
mass_kg = result['mass_kg'] # kg
|
||||||
|
volume = result['volume_mm3'] # mm³
|
||||||
|
area = result['surface_area_mm2'] # mm²
|
||||||
|
cog = result['center_of_gravity_mm'] # [x, y, z] mm
|
||||||
|
material = result['material']['name'] # e.g., "Aluminum_2014"
|
||||||
|
|
||||||
|
# Simple mass-only extraction
|
||||||
|
mass_kg = extract_part_mass(prt_file) # kg
|
||||||
|
```
|
||||||
|
|
||||||
|
**Return Dictionary**:
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'mass_kg': 0.1098, # Mass in kg
|
||||||
|
'mass_g': 109.84, # Mass in grams
|
||||||
|
'volume_mm3': 39311.99, # Volume in mm³
|
||||||
|
'surface_area_mm2': 10876.71, # Surface area in mm²
|
||||||
|
'center_of_gravity_mm': [0, 42.3, 39.6], # CoG in mm
|
||||||
|
'material': {
|
||||||
|
'name': 'Aluminum_2014', # Material name (or None)
|
||||||
|
'density': None, # Density if available
|
||||||
|
'density_unit': 'kg/mm^3'
|
||||||
|
},
|
||||||
|
'num_bodies': 1 # Number of solid bodies
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prerequisites**: Run the NX journal first to create the temp file:
|
||||||
|
```bash
|
||||||
|
run_journal.exe nx_journals/extract_part_mass_material.py -args model.prt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Extractor Selection Guide
|
||||||
|
|
||||||
|
| Need | Extractor | When to Use |
|
||||||
|
|------|-----------|-------------|
|
||||||
|
| Max deflection | E1 | Static analysis displacement check |
|
||||||
|
| Natural frequency | E2 | Modal analysis, resonance avoidance |
|
||||||
|
| Peak stress | E3 | Strength validation, fatigue life |
|
||||||
|
| FEM mass | E4 | When mass is from mesh elements |
|
||||||
|
| CAD mass | E5 | When mass is from NX expression |
|
||||||
|
| Temperature/Custom | E6 | Thermal or custom field results |
|
||||||
|
| k = F/δ | E7 | Stiffness maximization |
|
||||||
|
| Wavefront error | E8-E10 | Telescope/mirror optimization |
|
||||||
|
| Part mass + material | E11 | Direct from .prt file with material info |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Engineering Result Types
|
||||||
|
|
||||||
|
| Result Type | Nastran SOL | Output File | Extractor |
|
||||||
|
|-------------|-------------|-------------|-----------|
|
||||||
|
| Static Stress | SOL 101 | `.op2` | E3: `extract_solid_stress` |
|
||||||
|
| Displacement | SOL 101 | `.op2` | E1: `extract_displacement` |
|
||||||
|
| Natural Frequency | SOL 103 | `.op2` | E2: `extract_frequency` |
|
||||||
|
| Buckling Load | SOL 105 | `.op2` | `extract_buckling` |
|
||||||
|
| Modal Shapes | SOL 103 | `.op2` | `extract_mode_shapes` |
|
||||||
|
| Mass | - | `.dat`/`.bdf` | E4: `bdf_mass_extractor` |
|
||||||
|
| Stiffness | SOL 101 | `.fld` + `.op2` | E7: `stiffness_calculator` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Objective Formulations
|
||||||
|
|
||||||
|
### Stiffness Maximization
|
||||||
|
- k = F/δ (force/displacement)
|
||||||
|
- Maximize k or minimize 1/k (compliance)
|
||||||
|
- Requires consistent load magnitude across trials
|
||||||
|
|
||||||
|
### Mass Minimization
|
||||||
|
- Extract from BDF element properties + material density
|
||||||
|
- Units: typically kg (NX uses kg-mm-s)
|
||||||
|
|
||||||
|
### Stress Constraints
|
||||||
|
- Von Mises < σ_yield / safety_factor
|
||||||
|
- Account for stress concentrations
|
||||||
|
|
||||||
|
### Frequency Constraints
|
||||||
|
- f₁ > threshold (avoid resonance)
|
||||||
|
- Often paired with mass minimization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding New Extractors
|
||||||
|
|
||||||
|
When the study needs result extraction not covered by existing extractors (E1-E10):
|
||||||
|
|
||||||
|
```
|
||||||
|
STEP 1: Check existing extractors in this catalog
|
||||||
|
├── If exists → IMPORT and USE it (done!)
|
||||||
|
└── If missing → Continue to STEP 2
|
||||||
|
|
||||||
|
STEP 2: Create extractor in optimization_engine/extractors/
|
||||||
|
├── File: extract_{feature}.py
|
||||||
|
├── Follow existing extractor patterns
|
||||||
|
└── Include comprehensive docstrings
|
||||||
|
|
||||||
|
STEP 3: Add to __init__.py
|
||||||
|
└── Export functions in optimization_engine/extractors/__init__.py
|
||||||
|
|
||||||
|
STEP 4: Update this module
|
||||||
|
├── Add to Extractor Catalog table
|
||||||
|
└── Add code snippet
|
||||||
|
|
||||||
|
STEP 5: Document in SYS_12_EXTRACTOR_LIBRARY.md
|
||||||
|
```
|
||||||
|
|
||||||
|
See [EXT_01_CREATE_EXTRACTOR](../../docs/protocols/extensions/EXT_01_CREATE_EXTRACTOR.md) for full guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **System Protocol**: [SYS_12_EXTRACTOR_LIBRARY](../../docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md)
|
||||||
|
- **Extension Guide**: [EXT_01_CREATE_EXTRACTOR](../../docs/protocols/extensions/EXT_01_CREATE_EXTRACTOR.md)
|
||||||
|
- **Zernike Extractors**: [zernike-optimization module](./zernike-optimization.md)
|
||||||
|
- **Core Skill**: [study-creation-core](../core/study-creation-core.md)
|
||||||
340
.claude/skills/modules/neural-acceleration.md
Normal file
340
.claude/skills/modules/neural-acceleration.md
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
# Neural Acceleration Module
|
||||||
|
|
||||||
|
**Last Updated**: December 5, 2025
|
||||||
|
**Version**: 1.0
|
||||||
|
**Type**: Optional Module
|
||||||
|
|
||||||
|
This module provides guidance for AtomizerField neural network surrogate acceleration, enabling 1000x faster optimization by replacing expensive FEA evaluations with instant neural predictions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Load
|
||||||
|
|
||||||
|
- User needs >50 optimization trials
|
||||||
|
- User mentions "neural", "surrogate", "NN", "machine learning"
|
||||||
|
- User wants faster optimization
|
||||||
|
- Exporting training data for neural networks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**Key Innovation**: Train once on FEA data, then explore 50,000+ designs in the time it takes to run 50 FEA trials.
|
||||||
|
|
||||||
|
| Metric | Traditional FEA | Neural Network | Improvement |
|
||||||
|
|--------|-----------------|----------------|-------------|
|
||||||
|
| Time per evaluation | 10-30 minutes | 4.5 milliseconds | **2,000-500,000x** |
|
||||||
|
| Trials per hour | 2-6 | 800,000+ | **1000x** |
|
||||||
|
| Design exploration | ~50 designs | ~50,000 designs | **1000x** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Training Data Export (PR.9)
|
||||||
|
|
||||||
|
Enable training data export in your optimization config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"training_data_export": {
|
||||||
|
"enabled": true,
|
||||||
|
"export_dir": "atomizer_field_training_data/my_study"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using TrainingDataExporter
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.training_data_exporter import TrainingDataExporter
|
||||||
|
|
||||||
|
training_exporter = TrainingDataExporter(
|
||||||
|
export_dir=export_dir,
|
||||||
|
study_name=study_name,
|
||||||
|
design_variable_names=['param1', 'param2'],
|
||||||
|
objective_names=['stiffness', 'mass'],
|
||||||
|
constraint_names=['mass_limit'],
|
||||||
|
metadata={'atomizer_version': '2.0', 'optimization_algorithm': 'NSGA-II'}
|
||||||
|
)
|
||||||
|
|
||||||
|
# In objective function:
|
||||||
|
training_exporter.export_trial(
|
||||||
|
trial_number=trial.number,
|
||||||
|
design_variables=design_vars,
|
||||||
|
results={'objectives': {...}, 'constraints': {...}},
|
||||||
|
simulation_files={'dat_file': dat_path, 'op2_file': op2_path}
|
||||||
|
)
|
||||||
|
|
||||||
|
# After optimization:
|
||||||
|
training_exporter.finalize()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Training Data Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
atomizer_field_training_data/{study_name}/
|
||||||
|
├── trial_0001/
|
||||||
|
│ ├── input/model.bdf # Nastran input (mesh + params)
|
||||||
|
│ ├── output/model.op2 # Binary results
|
||||||
|
│ └── metadata.json # Design params + objectives
|
||||||
|
├── trial_0002/
|
||||||
|
│ └── ...
|
||||||
|
└── study_summary.json # Study-level metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended**: 100-500 FEA samples for good generalization.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Neural Configuration
|
||||||
|
|
||||||
|
### Full Configuration Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"study_name": "bracket_neural_optimization",
|
||||||
|
|
||||||
|
"surrogate_settings": {
|
||||||
|
"enabled": true,
|
||||||
|
"model_type": "parametric_gnn",
|
||||||
|
"model_path": "models/bracket_surrogate.pt",
|
||||||
|
"confidence_threshold": 0.85,
|
||||||
|
"validation_frequency": 10,
|
||||||
|
"fallback_to_fea": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"training_data_export": {
|
||||||
|
"enabled": true,
|
||||||
|
"export_dir": "atomizer_field_training_data/bracket_study",
|
||||||
|
"export_bdf": true,
|
||||||
|
"export_op2": true,
|
||||||
|
"export_fields": ["displacement", "stress"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"neural_optimization": {
|
||||||
|
"initial_fea_trials": 50,
|
||||||
|
"neural_trials": 5000,
|
||||||
|
"retraining_interval": 500,
|
||||||
|
"uncertainty_threshold": 0.15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | false | Enable neural surrogate |
|
||||||
|
| `model_type` | string | "parametric_gnn" | Model architecture |
|
||||||
|
| `model_path` | string | - | Path to trained model |
|
||||||
|
| `confidence_threshold` | float | 0.85 | Min confidence for predictions |
|
||||||
|
| `validation_frequency` | int | 10 | FEA validation every N trials |
|
||||||
|
| `fallback_to_fea` | bool | true | Use FEA when uncertain |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Model Types
|
||||||
|
|
||||||
|
### Parametric Predictor GNN (Recommended)
|
||||||
|
|
||||||
|
Direct optimization objective prediction - fastest option.
|
||||||
|
|
||||||
|
```
|
||||||
|
Design Parameters (ND) → Design Encoder (MLP) → GNN Backbone → Scalar Heads
|
||||||
|
|
||||||
|
Output (objectives):
|
||||||
|
├── mass (grams)
|
||||||
|
├── frequency (Hz)
|
||||||
|
├── max_displacement (mm)
|
||||||
|
└── max_stress (MPa)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use When**: You only need scalar objectives, not full field predictions.
|
||||||
|
|
||||||
|
### Field Predictor GNN
|
||||||
|
|
||||||
|
Full displacement/stress field prediction.
|
||||||
|
|
||||||
|
```
|
||||||
|
Input Features (12D per node):
|
||||||
|
├── Node coordinates (x, y, z)
|
||||||
|
├── Material properties (E, nu, rho)
|
||||||
|
├── Boundary conditions (fixed/free per DOF)
|
||||||
|
└── Load information (force magnitude, direction)
|
||||||
|
|
||||||
|
Output (per node):
|
||||||
|
├── Displacement (6 DOF: Tx, Ty, Tz, Rx, Ry, Rz)
|
||||||
|
└── Von Mises stress (1 value)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use When**: You need field visualization or complex derived quantities.
|
||||||
|
|
||||||
|
### Ensemble Models
|
||||||
|
|
||||||
|
Multiple models for uncertainty quantification.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Run N models
|
||||||
|
predictions = [model_i(x) for model_i in ensemble]
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
mean_prediction = np.mean(predictions)
|
||||||
|
uncertainty = np.std(predictions)
|
||||||
|
|
||||||
|
# Decision
|
||||||
|
if uncertainty > threshold:
|
||||||
|
result = run_fea(x) # Fall back to FEA
|
||||||
|
else:
|
||||||
|
result = mean_prediction
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hybrid FEA/Neural Workflow
|
||||||
|
|
||||||
|
### Phase 1: FEA Exploration (50-100 trials)
|
||||||
|
- Run standard FEA optimization
|
||||||
|
- Export training data automatically
|
||||||
|
- Build landscape understanding
|
||||||
|
|
||||||
|
### Phase 2: Neural Training
|
||||||
|
- Parse collected data
|
||||||
|
- Train parametric predictor
|
||||||
|
- Validate accuracy
|
||||||
|
|
||||||
|
### Phase 3: Neural Acceleration (1000s of trials)
|
||||||
|
- Use neural network for rapid exploration
|
||||||
|
- Periodic FEA validation
|
||||||
|
- Retrain if distribution shifts
|
||||||
|
|
||||||
|
### Phase 4: FEA Refinement (10-20 trials)
|
||||||
|
- Validate top candidates with FEA
|
||||||
|
- Ensure results are physically accurate
|
||||||
|
- Generate final Pareto front
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Training Pipeline
|
||||||
|
|
||||||
|
### Step 1: Collect Training Data
|
||||||
|
|
||||||
|
Run optimization with export enabled:
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --train --trials 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Parse to Neural Format
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd atomizer-field
|
||||||
|
python batch_parser.py ../atomizer_field_training_data/my_study
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Train Model
|
||||||
|
|
||||||
|
**Parametric Predictor** (recommended):
|
||||||
|
```bash
|
||||||
|
python train_parametric.py \
|
||||||
|
--train_dir ../training_data/parsed \
|
||||||
|
--val_dir ../validation_data/parsed \
|
||||||
|
--epochs 200 \
|
||||||
|
--hidden_channels 128 \
|
||||||
|
--num_layers 4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field Predictor**:
|
||||||
|
```bash
|
||||||
|
python train.py \
|
||||||
|
--train_dir ../training_data/parsed \
|
||||||
|
--epochs 200 \
|
||||||
|
--model FieldPredictorGNN \
|
||||||
|
--hidden_channels 128 \
|
||||||
|
--num_layers 6 \
|
||||||
|
--physics_loss_weight 0.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Validate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python validate.py --checkpoint runs/my_model/checkpoint_best.pt
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
Validation Results:
|
||||||
|
├── Mean Absolute Error: 2.3% (mass), 1.8% (frequency)
|
||||||
|
├── R² Score: 0.987
|
||||||
|
├── Inference Time: 4.5ms ± 0.8ms
|
||||||
|
└── Physics Violations: 0.2%
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Deploy
|
||||||
|
|
||||||
|
Update config to use trained model:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"neural_surrogate": {
|
||||||
|
"enabled": true,
|
||||||
|
"model_checkpoint": "atomizer-field/runs/my_model/checkpoint_best.pt",
|
||||||
|
"confidence_threshold": 0.85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uncertainty Thresholds
|
||||||
|
|
||||||
|
| Uncertainty | Action |
|
||||||
|
|-------------|--------|
|
||||||
|
| < 5% | Use neural prediction |
|
||||||
|
| 5-15% | Use neural, flag for validation |
|
||||||
|
| > 15% | Fall back to FEA |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accuracy Expectations
|
||||||
|
|
||||||
|
| Problem Type | Expected R² | Samples Needed |
|
||||||
|
|--------------|-------------|----------------|
|
||||||
|
| Well-behaved | > 0.95 | 50-100 |
|
||||||
|
| Moderate nonlinear | > 0.90 | 100-200 |
|
||||||
|
| Highly nonlinear | > 0.85 | 200-500 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AtomizerField Components
|
||||||
|
|
||||||
|
```
|
||||||
|
atomizer-field/
|
||||||
|
├── neural_field_parser.py # BDF/OP2 parsing
|
||||||
|
├── field_predictor.py # Field GNN
|
||||||
|
├── parametric_predictor.py # Parametric GNN
|
||||||
|
├── train.py # Field training
|
||||||
|
├── train_parametric.py # Parametric training
|
||||||
|
├── validate.py # Model validation
|
||||||
|
├── physics_losses.py # Physics-informed loss
|
||||||
|
└── batch_parser.py # Batch data conversion
|
||||||
|
|
||||||
|
optimization_engine/
|
||||||
|
├── neural_surrogate.py # Atomizer integration
|
||||||
|
└── runner_with_neural.py # Neural runner
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| High prediction error | Insufficient training data | Collect more FEA samples |
|
||||||
|
| Out-of-distribution warnings | Design outside training range | Retrain with expanded range |
|
||||||
|
| Slow inference | Large mesh | Use parametric predictor instead |
|
||||||
|
| Physics violations | Low physics loss weight | Increase `physics_loss_weight` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **System Protocol**: [SYS_14_NEURAL_ACCELERATION](../../docs/protocols/system/SYS_14_NEURAL_ACCELERATION.md)
|
||||||
|
- **Operations**: [OP_05_EXPORT_TRAINING_DATA](../../docs/protocols/operations/OP_05_EXPORT_TRAINING_DATA.md)
|
||||||
|
- **Core Skill**: [study-creation-core](../core/study-creation-core.md)
|
||||||
209
.claude/skills/modules/nx-docs-lookup.md
Normal file
209
.claude/skills/modules/nx-docs-lookup.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# NX Documentation Lookup Module
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This module provides on-demand access to Siemens NX Open and Simcenter documentation via the Dalidou MCP server. Use these tools when building new extractors, NX automation scripts, or debugging NX-related issues.
|
||||||
|
|
||||||
|
## CRITICAL: When to AUTO-SEARCH Documentation
|
||||||
|
|
||||||
|
**You MUST call `siemens_docs_search` BEFORE writing any code that uses NX Open APIs.**
|
||||||
|
|
||||||
|
### Automatic Search Triggers
|
||||||
|
|
||||||
|
| User Request | Action Required |
|
||||||
|
|--------------|-----------------|
|
||||||
|
| "Create extractor for {X}" | → `siemens_docs_search("{X} NXOpen")` |
|
||||||
|
| "Get {property} from part" | → `siemens_docs_search("{property} NXOpen.Part")` |
|
||||||
|
| "Extract {data} from FEM" | → `siemens_docs_search("{data} NXOpen.CAE")` |
|
||||||
|
| "How do I {action} in NX" | → `siemens_docs_search("{action} NXOpen")` |
|
||||||
|
| Any code with `NXOpen.*` | → Search before writing |
|
||||||
|
|
||||||
|
### Example: User asks "Create an extractor for inertia values"
|
||||||
|
|
||||||
|
```
|
||||||
|
STEP 1: Immediately search
|
||||||
|
→ siemens_docs_search("inertia mass properties NXOpen")
|
||||||
|
|
||||||
|
STEP 2: Review results, fetch details
|
||||||
|
→ siemens_docs_fetch("NXOpen.MeasureManager")
|
||||||
|
|
||||||
|
STEP 3: Now write code with correct API calls
|
||||||
|
```
|
||||||
|
|
||||||
|
**DO NOT guess NX Open API names.** Always search first.
|
||||||
|
|
||||||
|
## When to Load
|
||||||
|
|
||||||
|
Load this module when:
|
||||||
|
- Creating new NX Open scripts or extractors
|
||||||
|
- Working with `NXOpen.*` namespaces
|
||||||
|
- Debugging NX automation errors
|
||||||
|
- User mentions "NX API", "NX Open", "Simcenter docs"
|
||||||
|
- Building features that interact with NX/Simcenter
|
||||||
|
|
||||||
|
## Available MCP Tools
|
||||||
|
|
||||||
|
### `siemens_docs_search`
|
||||||
|
|
||||||
|
**Purpose**: Search across NX Open, Simcenter, and Teamcenter documentation
|
||||||
|
|
||||||
|
**When to use**:
|
||||||
|
- Finding which class/method performs a specific task
|
||||||
|
- Discovering available APIs for a feature
|
||||||
|
- Looking up Nastran card references
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
```
|
||||||
|
siemens_docs_search("get node coordinates FEM")
|
||||||
|
siemens_docs_search("CQUAD4 element properties")
|
||||||
|
siemens_docs_search("NXOpen.CAE mesh creation")
|
||||||
|
siemens_docs_search("extract stress results OP2")
|
||||||
|
```
|
||||||
|
|
||||||
|
### `siemens_docs_fetch`
|
||||||
|
|
||||||
|
**Purpose**: Fetch a specific documentation page with full content
|
||||||
|
|
||||||
|
**When to use**:
|
||||||
|
- Need complete class reference
|
||||||
|
- Getting detailed method signatures
|
||||||
|
- Reading full examples
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
```
|
||||||
|
siemens_docs_fetch("NXOpen.CAE.FemPart")
|
||||||
|
siemens_docs_fetch("Nastran Quick Reference CQUAD4")
|
||||||
|
```
|
||||||
|
|
||||||
|
### `siemens_auth_status`
|
||||||
|
|
||||||
|
**Purpose**: Check if the Siemens SSO session is valid
|
||||||
|
|
||||||
|
**When to use**:
|
||||||
|
- Before a series of documentation lookups
|
||||||
|
- When fetch requests fail
|
||||||
|
- Debugging connection issues
|
||||||
|
|
||||||
|
### `siemens_login`
|
||||||
|
|
||||||
|
**Purpose**: Re-authenticate with Siemens if session expired
|
||||||
|
|
||||||
|
**When to use**:
|
||||||
|
- After `siemens_auth_status` shows expired
|
||||||
|
- When documentation fetches return auth errors
|
||||||
|
|
||||||
|
## Workflow: Building New Extractor
|
||||||
|
|
||||||
|
When creating a new extractor that uses NX Open APIs:
|
||||||
|
|
||||||
|
### Step 1: Search for Relevant APIs
|
||||||
|
```
|
||||||
|
→ siemens_docs_search("element stress results OP2")
|
||||||
|
```
|
||||||
|
Review results to identify candidate classes/methods.
|
||||||
|
|
||||||
|
### Step 2: Fetch Detailed Documentation
|
||||||
|
```
|
||||||
|
→ siemens_docs_fetch("NXOpen.CAE.Result")
|
||||||
|
```
|
||||||
|
Get full class documentation with method signatures.
|
||||||
|
|
||||||
|
### Step 3: Understand Data Formats
|
||||||
|
```
|
||||||
|
→ siemens_docs_search("CQUAD4 stress output format")
|
||||||
|
```
|
||||||
|
Understand Nastran output structure.
|
||||||
|
|
||||||
|
### Step 4: Build Extractor
|
||||||
|
Following EXT_01 template, create the extractor with:
|
||||||
|
- Proper API calls based on documentation
|
||||||
|
- Docstring referencing the APIs used
|
||||||
|
- Error handling for common NX exceptions
|
||||||
|
|
||||||
|
### Step 5: Document API Usage
|
||||||
|
In the extractor docstring:
|
||||||
|
```python
|
||||||
|
def extract_element_stress(op2_path: Path) -> Dict:
|
||||||
|
"""
|
||||||
|
Extract element stress results from OP2 file.
|
||||||
|
|
||||||
|
NX Open APIs Used:
|
||||||
|
- NXOpen.CAE.Result.AskElementStress
|
||||||
|
- NXOpen.CAE.ResultAccess.AskResultValues
|
||||||
|
|
||||||
|
Nastran Cards:
|
||||||
|
- CQUAD4, CTRIA3 (shell elements)
|
||||||
|
- STRESS case control
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow: Debugging NX Errors
|
||||||
|
|
||||||
|
When encountering NX Open errors:
|
||||||
|
|
||||||
|
### Step 1: Search for Correct API
|
||||||
|
```
|
||||||
|
Error: AttributeError: 'FemPart' object has no attribute 'GetNodes'
|
||||||
|
|
||||||
|
→ siemens_docs_search("FemPart get nodes")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Fetch Correct Class Reference
|
||||||
|
```
|
||||||
|
→ siemens_docs_fetch("NXOpen.CAE.FemPart")
|
||||||
|
```
|
||||||
|
Find the actual method name and signature.
|
||||||
|
|
||||||
|
### Step 3: Apply Fix
|
||||||
|
Document the correction:
|
||||||
|
```python
|
||||||
|
# Wrong: femPart.GetNodes()
|
||||||
|
# Right: femPart.BaseFEModel.FemMesh.Nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Search Patterns
|
||||||
|
|
||||||
|
| Task | Search Query |
|
||||||
|
|------|--------------|
|
||||||
|
| Mesh operations | `siemens_docs_search("NXOpen.CAE mesh")` |
|
||||||
|
| Result extraction | `siemens_docs_search("CAE result OP2")` |
|
||||||
|
| Geometry access | `siemens_docs_search("NXOpen.Features body")` |
|
||||||
|
| Material properties | `siemens_docs_search("Nastran MAT1 material")` |
|
||||||
|
| Load application | `siemens_docs_search("CAE load force")` |
|
||||||
|
| Constraint setup | `siemens_docs_search("CAE boundary condition")` |
|
||||||
|
| Expressions/Parameters | `siemens_docs_search("NXOpen Expression")` |
|
||||||
|
| Part manipulation | `siemens_docs_search("NXOpen.Part")` |
|
||||||
|
|
||||||
|
## Key NX Open Namespaces
|
||||||
|
|
||||||
|
| Namespace | Domain |
|
||||||
|
|-----------|--------|
|
||||||
|
| `NXOpen.CAE` | FEA, meshing, results |
|
||||||
|
| `NXOpen.Features` | Parametric features |
|
||||||
|
| `NXOpen.Assemblies` | Assembly operations |
|
||||||
|
| `NXOpen.Part` | Part-level operations |
|
||||||
|
| `NXOpen.UF` | User Function (legacy) |
|
||||||
|
| `NXOpen.GeometricUtilities` | Geometry helpers |
|
||||||
|
|
||||||
|
## Integration with Extractors
|
||||||
|
|
||||||
|
All extractors in `optimization_engine/extractors/` should:
|
||||||
|
|
||||||
|
1. **Search before coding**: Use `siemens_docs_search` to find correct APIs
|
||||||
|
2. **Document API usage**: List NX Open APIs in docstring
|
||||||
|
3. **Handle NX exceptions**: Catch `NXOpen.NXException` appropriately
|
||||||
|
4. **Follow 20-line rule**: If extraction is complex, check if existing extractor handles it
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| Auth errors | Run `siemens_auth_status`, then `siemens_login` if needed |
|
||||||
|
| No results | Try broader search terms, check namespace spelling |
|
||||||
|
| Incomplete docs | Fetch the parent class for full context |
|
||||||
|
| Network errors | Verify Dalidou is accessible: `ping dalidou.local` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Module Version: 1.0*
|
||||||
|
*MCP Server: dalidou.local:5000*
|
||||||
364
.claude/skills/modules/zernike-optimization.md
Normal file
364
.claude/skills/modules/zernike-optimization.md
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# Zernike Optimization Module
|
||||||
|
|
||||||
|
**Last Updated**: December 5, 2025
|
||||||
|
**Version**: 1.0
|
||||||
|
**Type**: Optional Module
|
||||||
|
|
||||||
|
This module provides specialized guidance for telescope mirror and optical surface optimization using Zernike polynomial decomposition.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Load
|
||||||
|
|
||||||
|
- User mentions "telescope", "mirror", "optical", "wavefront"
|
||||||
|
- Optimization involves surface deformation analysis
|
||||||
|
- Need to extract Zernike coefficients from FEA results
|
||||||
|
- Working with multi-subcase elevation angle comparisons
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zernike Extractors (E8-E10)
|
||||||
|
|
||||||
|
| ID | Extractor | Function | Input | Output | Use Case |
|
||||||
|
|----|-----------|----------|-------|--------|----------|
|
||||||
|
| E8 | **Zernike WFE** | `extract_zernike_from_op2()` | `.op2` + `.bdf` | nm | Single subcase wavefront error |
|
||||||
|
| E9 | **Zernike Relative** | `extract_zernike_relative_rms()` | `.op2` + `.bdf` | nm | Compare target vs reference subcase |
|
||||||
|
| E10 | **Zernike Helpers** | `ZernikeObjectiveBuilder` | `.op2` | nm | Multi-subcase optimization builder |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E8: Single Subcase Zernike Extraction
|
||||||
|
|
||||||
|
Extract Zernike coefficients and RMS metrics for a single subcase (e.g., one elevation angle).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors.extract_zernike import extract_zernike_from_op2
|
||||||
|
|
||||||
|
# Extract Zernike coefficients and RMS metrics for a single subcase
|
||||||
|
result = extract_zernike_from_op2(
|
||||||
|
op2_file,
|
||||||
|
bdf_file=None, # Auto-detect from op2 location
|
||||||
|
subcase="20", # Subcase label (e.g., "20" = 20 deg elevation)
|
||||||
|
displacement_unit="mm"
|
||||||
|
)
|
||||||
|
|
||||||
|
global_rms = result['global_rms_nm'] # Total surface RMS in nm
|
||||||
|
filtered_rms = result['filtered_rms_nm'] # RMS with low orders removed
|
||||||
|
coefficients = result['coefficients'] # List of 50 Zernike coefficients
|
||||||
|
```
|
||||||
|
|
||||||
|
**Return Dictionary**:
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'global_rms_nm': 45.2, # Total surface RMS (nm)
|
||||||
|
'filtered_rms_nm': 12.8, # RMS with J1-J4 (piston, tip, tilt, defocus) removed
|
||||||
|
'coefficients': [0.0, 12.3, ...], # 50 Zernike coefficients (Noll indexing)
|
||||||
|
'n_nodes': 5432, # Number of surface nodes
|
||||||
|
'rms_per_mode': {...} # RMS contribution per Zernike mode
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to Use**:
|
||||||
|
- Single elevation angle analysis
|
||||||
|
- Polishing orientation (zenith) wavefront error
|
||||||
|
- Absolute surface quality metrics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E9: Relative RMS Between Subcases
|
||||||
|
|
||||||
|
Compare wavefront error between two subcases (e.g., 40° vs 20° reference).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors.extract_zernike import extract_zernike_relative_rms
|
||||||
|
|
||||||
|
# Compare wavefront error between subcases (e.g., 40 deg vs 20 deg reference)
|
||||||
|
result = extract_zernike_relative_rms(
|
||||||
|
op2_file,
|
||||||
|
bdf_file=None,
|
||||||
|
target_subcase="40", # Target orientation
|
||||||
|
reference_subcase="20", # Reference (usually polishing orientation)
|
||||||
|
displacement_unit="mm"
|
||||||
|
)
|
||||||
|
|
||||||
|
relative_rms = result['relative_filtered_rms_nm'] # Differential WFE in nm
|
||||||
|
delta_coeffs = result['delta_coefficients'] # Coefficient differences
|
||||||
|
```
|
||||||
|
|
||||||
|
**Return Dictionary**:
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'relative_filtered_rms_nm': 8.7, # Differential WFE (target - reference)
|
||||||
|
'delta_coefficients': [...], # Coefficient differences
|
||||||
|
'target_rms_nm': 52.3, # Target subcase absolute RMS
|
||||||
|
'reference_rms_nm': 45.2, # Reference subcase absolute RMS
|
||||||
|
'improvement_percent': -15.7 # Negative = worse than reference
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to Use**:
|
||||||
|
- Comparing performance across elevation angles
|
||||||
|
- Minimizing deformation relative to polishing orientation
|
||||||
|
- Multi-angle telescope mirror optimization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E10: Multi-Subcase Objective Builder
|
||||||
|
|
||||||
|
Build objectives for multiple subcases in a single extractor (most efficient for complex optimization).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.extractors.zernike_helpers import ZernikeObjectiveBuilder
|
||||||
|
|
||||||
|
# Build objectives for multiple subcases in one extractor
|
||||||
|
builder = ZernikeObjectiveBuilder(
|
||||||
|
op2_finder=lambda: model_dir / "ASSY_M1-solution_1.op2"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add relative objectives (target vs reference)
|
||||||
|
builder.add_relative_objective(
|
||||||
|
"40", "20", # 40° vs 20° reference
|
||||||
|
metric="relative_filtered_rms_nm",
|
||||||
|
weight=5.0
|
||||||
|
)
|
||||||
|
builder.add_relative_objective(
|
||||||
|
"60", "20", # 60° vs 20° reference
|
||||||
|
metric="relative_filtered_rms_nm",
|
||||||
|
weight=5.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add absolute objective for polishing orientation
|
||||||
|
builder.add_subcase_objective(
|
||||||
|
"90", # Zenith (polishing orientation)
|
||||||
|
metric="rms_filter_j1to3", # Only remove piston, tip, tilt
|
||||||
|
weight=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Evaluate all at once (efficient - parses OP2 only once)
|
||||||
|
results = builder.evaluate_all()
|
||||||
|
# Returns: {'rel_40_vs_20': 4.2, 'rel_60_vs_20': 8.7, 'rms_90': 15.3}
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to Use**:
|
||||||
|
- Multi-objective telescope optimization
|
||||||
|
- Multiple elevation angles to optimize
|
||||||
|
- Weighted combination of absolute and relative WFE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zernike Modes Reference
|
||||||
|
|
||||||
|
| Noll Index | Name | Physical Meaning | Correctability |
|
||||||
|
|------------|------|------------------|----------------|
|
||||||
|
| J1 | Piston | Constant offset | Easily corrected |
|
||||||
|
| J2 | Tip | X-tilt | Easily corrected |
|
||||||
|
| J3 | Tilt | Y-tilt | Easily corrected |
|
||||||
|
| J4 | Defocus | Power error | Easily corrected |
|
||||||
|
| J5 | Astigmatism (0°) | Cylindrical error | Correctable |
|
||||||
|
| J6 | Astigmatism (45°) | Cylindrical error | Correctable |
|
||||||
|
| J7 | Coma (x) | Off-axis aberration | Harder to correct |
|
||||||
|
| J8 | Coma (y) | Off-axis aberration | Harder to correct |
|
||||||
|
| J9-J10 | Trefoil | Triangular error | Hard to correct |
|
||||||
|
| J11+ | Higher order | Complex aberrations | Very hard to correct |
|
||||||
|
|
||||||
|
**Filtering Convention**:
|
||||||
|
- `filtered_rms`: Removes J1-J4 (piston, tip, tilt, defocus) - standard
|
||||||
|
- `rms_filter_j1to3`: Removes only J1-J3 (keeps defocus) - for focus-sensitive applications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Zernike Optimization Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Minimize Relative WFE Across Elevations
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Objective: Minimize max relative WFE across all elevation angles
|
||||||
|
objectives = [
|
||||||
|
{"name": "rel_40_vs_20", "goal": "minimize"},
|
||||||
|
{"name": "rel_60_vs_20", "goal": "minimize"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Use weighted sum or multi-objective
|
||||||
|
def objective(trial):
|
||||||
|
results = builder.evaluate_all()
|
||||||
|
return (results['rel_40_vs_20'], results['rel_60_vs_20'])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Single Elevation + Mass
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Objective: Minimize WFE at 45° while minimizing mass
|
||||||
|
objectives = [
|
||||||
|
{"name": "wfe_45", "goal": "minimize"}, # Wavefront error
|
||||||
|
{"name": "mass", "goal": "minimize"}, # Mirror mass
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Weighted Multi-Angle
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Weighted combination of multiple angles
|
||||||
|
def combined_wfe(trial):
|
||||||
|
results = builder.evaluate_all()
|
||||||
|
weighted_wfe = (
|
||||||
|
5.0 * results['rel_40_vs_20'] +
|
||||||
|
5.0 * results['rel_60_vs_20'] +
|
||||||
|
1.0 * results['rms_90']
|
||||||
|
)
|
||||||
|
return weighted_wfe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Telescope Mirror Study Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"study_name": "m1_mirror_optimization",
|
||||||
|
"description": "Minimize wavefront error across elevation angles",
|
||||||
|
|
||||||
|
"objectives": [
|
||||||
|
{
|
||||||
|
"name": "wfe_40_vs_20",
|
||||||
|
"goal": "minimize",
|
||||||
|
"unit": "nm",
|
||||||
|
"extraction": {
|
||||||
|
"action": "extract_zernike_relative_rms",
|
||||||
|
"params": {
|
||||||
|
"target_subcase": "40",
|
||||||
|
"reference_subcase": "20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"simulation": {
|
||||||
|
"analysis_types": ["static"],
|
||||||
|
"subcases": ["20", "40", "60", "90"],
|
||||||
|
"solution_name": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
1. **Parse OP2 Once**: Use `ZernikeObjectiveBuilder` to parse the OP2 file only once per trial
|
||||||
|
2. **Subcase Labels**: Match exact subcase labels from NX simulation
|
||||||
|
3. **Node Selection**: Zernike extraction uses surface nodes only (auto-detected from BDF)
|
||||||
|
4. **Memory**: Large meshes (>50k nodes) may require chunked processing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| "Subcase not found" | Wrong subcase label | Check NX .sim for exact labels |
|
||||||
|
| High J1-J4 coefficients | Rigid body motion not constrained | Check boundary conditions |
|
||||||
|
| NaN in coefficients | Insufficient nodes for polynomial order | Reduce max Zernike order |
|
||||||
|
| Inconsistent RMS | Different node sets per subcase | Verify mesh consistency |
|
||||||
|
| "Billion nm" RMS values | Node merge failed in AFEM | Check `MergeOccurrenceNodes = True` |
|
||||||
|
| Corrupt OP2 data | All-zero displacements | Validate OP2 before processing |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assembly FEM (AFEM) Structure for Mirrors
|
||||||
|
|
||||||
|
Telescope mirror assemblies in NX typically consist of:
|
||||||
|
|
||||||
|
```
|
||||||
|
ASSY_M1.prt # Master assembly part
|
||||||
|
ASSY_M1_assyfem1.afm # Assembly FEM container
|
||||||
|
ASSY_M1_assyfem1_sim1.sim # Simulation file (solve this)
|
||||||
|
M1_Blank.prt # Mirror blank part
|
||||||
|
M1_Blank_fem1.fem # Mirror blank mesh
|
||||||
|
M1_Vertical_Support_Skeleton.prt # Support structure
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Point**: Expressions in master `.prt` propagate through assembly → AFEM updates automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-Subcase Gravity Analysis
|
||||||
|
|
||||||
|
For telescope mirrors, analyze multiple gravity orientations:
|
||||||
|
|
||||||
|
| Subcase | Elevation Angle | Purpose |
|
||||||
|
|---------|-----------------|---------|
|
||||||
|
| 1 | 90° (zenith) | Polishing orientation - manufacturing reference |
|
||||||
|
| 2 | 20° | Low elevation - reference for relative metrics |
|
||||||
|
| 3 | 40° | Mid-low elevation |
|
||||||
|
| 4 | 60° | Mid-high elevation |
|
||||||
|
|
||||||
|
**CRITICAL**: NX subcase numbers don't always match angle labels! Use explicit mapping:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"subcase_labels": {
|
||||||
|
"1": "90deg",
|
||||||
|
"2": "20deg",
|
||||||
|
"3": "40deg",
|
||||||
|
"4": "60deg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned (M1 Mirror V1-V9)
|
||||||
|
|
||||||
|
### 1. TPE Sampler Seed Issue
|
||||||
|
|
||||||
|
**Problem**: Resuming study with fixed seed causes duplicate parameters.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```python
|
||||||
|
if is_new_study:
|
||||||
|
sampler = TPESampler(seed=42)
|
||||||
|
else:
|
||||||
|
sampler = TPESampler() # No seed for resume
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. OP2 Data Validation
|
||||||
|
|
||||||
|
**Always validate before processing**:
|
||||||
|
```python
|
||||||
|
unique_values = len(np.unique(disp_z))
|
||||||
|
if unique_values < 10:
|
||||||
|
raise RuntimeError("CORRUPT OP2: insufficient unique values")
|
||||||
|
|
||||||
|
if np.abs(disp_z).max() > 1e6:
|
||||||
|
raise RuntimeError("CORRUPT OP2: unrealistic displacement")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Reference Subcase Selection
|
||||||
|
|
||||||
|
Use lowest operational elevation (typically 20°) as reference. Higher elevations show positive relative WFE as gravity effects increase.
|
||||||
|
|
||||||
|
### 4. Optical Convention
|
||||||
|
|
||||||
|
For mirror surface to wavefront error:
|
||||||
|
```python
|
||||||
|
WFE = 2 * surface_displacement # Reflection doubles path difference
|
||||||
|
wfe_nm = 2.0 * displacement_mm * 1e6 # Convert mm to nm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Typical Mirror Design Variables
|
||||||
|
|
||||||
|
| Parameter | Description | Typical Range |
|
||||||
|
|-----------|-------------|---------------|
|
||||||
|
| `whiffle_min` | Whiffle tree minimum dimension | 35-55 mm |
|
||||||
|
| `whiffle_outer_to_vertical` | Whiffle arm angle | 68-80 deg |
|
||||||
|
| `inner_circular_rib_dia` | Rib diameter | 480-620 mm |
|
||||||
|
| `lateral_inner_angle` | Lateral support angle | 25-28.5 deg |
|
||||||
|
| `blank_backface_angle` | Mirror blank geometry | 3.5-5.0 deg |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Extractor Catalog**: [extractors-catalog module](./extractors-catalog.md)
|
||||||
|
- **System Protocol**: [SYS_12_EXTRACTOR_LIBRARY](../../docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md)
|
||||||
|
- **Core Skill**: [study-creation-core](../core/study-creation-core.md)
|
||||||
362
CLAUDE.md
362
CLAUDE.md
@@ -2,280 +2,212 @@
|
|||||||
|
|
||||||
You are the AI orchestrator for **Atomizer**, an LLM-first FEA optimization framework. Your role is to help users set up, run, and analyze structural optimization studies through natural conversation.
|
You are the AI orchestrator for **Atomizer**, an LLM-first FEA optimization framework. Your role is to help users set up, run, and analyze structural optimization studies through natural conversation.
|
||||||
|
|
||||||
|
## Quick Start - Protocol Operating System
|
||||||
|
|
||||||
|
**For ANY task, first check**: `.claude/skills/00_BOOTSTRAP.md`
|
||||||
|
|
||||||
|
This file provides:
|
||||||
|
- Task classification (CREATE → RUN → MONITOR → ANALYZE → DEBUG)
|
||||||
|
- Protocol routing (which docs to load)
|
||||||
|
- Role detection (user / power_user / admin)
|
||||||
|
|
||||||
## Core Philosophy
|
## Core Philosophy
|
||||||
|
|
||||||
**Talk, don't click.** Users describe what they want in plain language. You interpret, configure, execute, and explain. The dashboard is for monitoring - you handle the setup.
|
**Talk, don't click.** Users describe what they want in plain language. You interpret, configure, execute, and explain.
|
||||||
|
|
||||||
## What Atomizer Does
|
## Context Loading Layers
|
||||||
|
|
||||||
Atomizer automates parametric FEA optimization using NX Nastran:
|
The Protocol Operating System (POS) provides layered documentation:
|
||||||
- User describes optimization goals in natural language
|
|
||||||
- You create configurations, scripts, and study structure
|
|
||||||
- NX Nastran runs FEA simulations
|
|
||||||
- Optuna optimizes design parameters
|
|
||||||
- Neural networks accelerate repeated evaluations
|
|
||||||
- Dashboard visualizes results in real-time
|
|
||||||
|
|
||||||
## Your Capabilities
|
| Layer | Location | When to Load |
|
||||||
|
|-------|----------|--------------|
|
||||||
|
| **Bootstrap** | `.claude/skills/00-02*.md` | Always (via this file) |
|
||||||
|
| **Operations** | `docs/protocols/operations/OP_*.md` | Per task type |
|
||||||
|
| **System** | `docs/protocols/system/SYS_*.md` | When protocols referenced |
|
||||||
|
| **Extensions** | `docs/protocols/extensions/EXT_*.md` | When extending (power_user+) |
|
||||||
|
|
||||||
### 1. Create Optimization Studies
|
**Context loading rules**: See `.claude/skills/02_CONTEXT_LOADER.md`
|
||||||
When user wants to optimize something:
|
|
||||||
- Gather requirements through conversation
|
|
||||||
- Read `.claude/skills/create-study.md` for the full protocol
|
|
||||||
- Generate all configuration files
|
|
||||||
- Validate setup before running
|
|
||||||
|
|
||||||
### 2. Analyze NX Models
|
## Task → Protocol Quick Lookup
|
||||||
When user provides NX files:
|
|
||||||
- Extract expressions (design parameters)
|
|
||||||
- Identify simulation setup
|
|
||||||
- Suggest optimization targets
|
|
||||||
- Check for multi-solution requirements
|
|
||||||
|
|
||||||
### 3. Run & Monitor Optimizations
|
| Task | Protocol | Key File |
|
||||||
- Start optimization runs
|
|------|----------|----------|
|
||||||
- Check progress in databases
|
| Create study | OP_01 | `docs/protocols/operations/OP_01_CREATE_STUDY.md` |
|
||||||
- Interpret results
|
| Run optimization | OP_02 | `docs/protocols/operations/OP_02_RUN_OPTIMIZATION.md` |
|
||||||
- Generate reports
|
| Check progress | OP_03 | `docs/protocols/operations/OP_03_MONITOR_PROGRESS.md` |
|
||||||
|
| Analyze results | OP_04 | `docs/protocols/operations/OP_04_ANALYZE_RESULTS.md` |
|
||||||
|
| Export neural data | OP_05 | `docs/protocols/operations/OP_05_EXPORT_TRAINING_DATA.md` |
|
||||||
|
| Debug issues | OP_06 | `docs/protocols/operations/OP_06_TROUBLESHOOT.md` |
|
||||||
|
|
||||||
### 4. Configure Neural Network Surrogates
|
## System Protocols (Technical Specs)
|
||||||
When optimization needs >50 trials:
|
|
||||||
- Generate space-filling training data
|
|
||||||
- Run parallel FEA for training
|
|
||||||
- Train and validate surrogates
|
|
||||||
- Enable accelerated optimization
|
|
||||||
|
|
||||||
### 5. Troubleshoot Issues
|
| # | Name | When to Load |
|
||||||
- Parse error logs
|
|---|------|--------------|
|
||||||
- Identify common problems
|
| 10 | IMSO (Adaptive) | Single-objective, "adaptive", "intelligent" |
|
||||||
- Suggest fixes
|
| 11 | Multi-Objective | 2+ objectives, "pareto", NSGA-II |
|
||||||
- Recover from failures
|
| 12 | Extractor Library | Any extraction, "displacement", "stress" |
|
||||||
|
| 13 | Dashboard | "dashboard", "real-time", monitoring |
|
||||||
|
| 14 | Neural Acceleration | >50 trials, "neural", "surrogate" |
|
||||||
|
|
||||||
|
**Full specs**: `docs/protocols/system/SYS_{N}_{NAME}.md`
|
||||||
|
|
||||||
## Python Environment
|
## Python Environment
|
||||||
|
|
||||||
**CRITICAL: Always use the `atomizer` conda environment.** All dependencies are pre-installed.
|
**CRITICAL: Always use the `atomizer` conda environment.**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Activate before ANY Python command
|
|
||||||
conda activate atomizer
|
conda activate atomizer
|
||||||
|
python run_optimization.py
|
||||||
# Then run scripts
|
|
||||||
python run_optimization.py --start
|
|
||||||
python -m optimization_engine.runner ...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**DO NOT:**
|
**DO NOT:**
|
||||||
- Install packages with pip/conda (everything is already installed)
|
- Install packages with pip/conda (everything is installed)
|
||||||
- Create new virtual environments
|
- Create new virtual environments
|
||||||
- Use system Python
|
- Use system Python
|
||||||
|
|
||||||
**Pre-installed packages include:** optuna, numpy, scipy, pandas, matplotlib, pyNastran, torch, plotly, and all Atomizer dependencies.
|
## Key Directories
|
||||||
|
|
||||||
## Key Files & Locations
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Atomizer/
|
Atomizer/
|
||||||
├── .claude/
|
├── .claude/skills/ # LLM skills (Bootstrap + Core + Modules)
|
||||||
│ ├── skills/ # Skill instructions (READ THESE)
|
├── docs/protocols/ # Protocol Operating System
|
||||||
│ │ ├── create-study.md # Main study creation skill
|
│ ├── operations/ # OP_01 - OP_06
|
||||||
│ │ └── analyze-workflow.md
|
│ ├── system/ # SYS_10 - SYS_14
|
||||||
│ └── settings.local.json
|
│ └── extensions/ # EXT_01 - EXT_04
|
||||||
├── docs/
|
|
||||||
│ ├── 01_PROTOCOLS.md # Quick protocol reference
|
|
||||||
│ ├── 06_PROTOCOLS_DETAILED/ # Full protocol docs
|
|
||||||
│ └── 07_DEVELOPMENT/ # Development plans
|
|
||||||
├── optimization_engine/ # Core Python modules
|
├── optimization_engine/ # Core Python modules
|
||||||
│ ├── runner.py # Main optimizer
|
│ └── extractors/ # Physics extraction library
|
||||||
│ ├── nx_solver.py # NX interface
|
├── studies/ # User studies
|
||||||
│ ├── extractors/ # Result extraction
|
|
||||||
│ └── validators/ # Config validation
|
|
||||||
├── studies/ # User studies live here
|
|
||||||
│ └── {study_name}/
|
|
||||||
│ ├── 1_setup/ # Config & model files
|
|
||||||
│ ├── 2_results/ # Optuna DB & outputs
|
|
||||||
│ └── run_optimization.py
|
|
||||||
└── atomizer-dashboard/ # React dashboard
|
└── atomizer-dashboard/ # React dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conversation Patterns
|
## CRITICAL: NX Open Development Protocol
|
||||||
|
|
||||||
### User: "I want to optimize this bracket"
|
### Always Use Official Documentation First
|
||||||
1. Ask about model location, goals, constraints
|
|
||||||
2. Load skill: `.claude/skills/create-study.md`
|
|
||||||
3. Follow the interactive discovery process
|
|
||||||
4. Generate files, validate, confirm
|
|
||||||
|
|
||||||
### User: "Run 200 trials with neural network"
|
**For ANY development involving NX, NX Open, or Siemens APIs:**
|
||||||
1. Check if surrogate_settings needed
|
|
||||||
2. Modify config to enable NN
|
|
||||||
3. Explain the hybrid workflow stages
|
|
||||||
4. Start run, show monitoring options
|
|
||||||
|
|
||||||
### User: "What's the status?"
|
1. **FIRST** - Query the MCP Siemens docs tools:
|
||||||
1. Query database for trial counts
|
- `mcp__siemens-docs__nxopen_get_class` - Get class documentation
|
||||||
2. Check for running background processes
|
- `mcp__siemens-docs__nxopen_get_index` - Browse class/function indexes
|
||||||
3. Summarize progress and best results
|
- `mcp__siemens-docs__siemens_docs_list` - List available resources
|
||||||
4. Suggest next steps
|
|
||||||
|
|
||||||
### User: "The optimization failed"
|
2. **THEN** - Use secondary sources if needed:
|
||||||
1. Read error logs
|
- PyNastran documentation (for BDF/OP2 parsing)
|
||||||
2. Check common failure modes
|
- NXOpen TSE examples in `nx_journals/`
|
||||||
3. Suggest fixes
|
- Existing extractors in `optimization_engine/extractors/`
|
||||||
4. Offer to retry
|
|
||||||
|
|
||||||
## Protocols Reference
|
3. **NEVER** - Guess NX Open API calls without checking documentation first
|
||||||
|
|
||||||
| Protocol | Use Case | Sampler |
|
**Available NX Open Classes (quick lookup):**
|
||||||
|----------|----------|---------|
|
| Class | Page ID | Description |
|
||||||
| Protocol 10 | Single objective + constraints | TPE/CMA-ES |
|
|-------|---------|-------------|
|
||||||
| Protocol 11 | Multi-objective (2-3 goals) | NSGA-II |
|
| Session | a03318.html | Main NX session object |
|
||||||
| Protocol 12 | Hybrid FEA/NN acceleration | NSGA-II + surrogate |
|
| Part | a02434.html | Part file operations |
|
||||||
|
| BasePart | a00266.html | Base class for parts |
|
||||||
## Result Extraction
|
| CaeSession | a10510.html | CAE/FEM session |
|
||||||
|
| PdmSession | a50542.html | PDM integration |
|
||||||
Use centralized extractors from `optimization_engine/extractors/`:
|
|
||||||
|
|
||||||
| Need | Extractor | Example |
|
|
||||||
|------|-----------|---------|
|
|
||||||
| Displacement | `extract_displacement` | Max tip deflection |
|
|
||||||
| Stress | `extract_solid_stress` | Max von Mises |
|
|
||||||
| Frequency | `extract_frequency` | 1st natural freq |
|
|
||||||
| Mass | `extract_mass_from_expression` | CAD mass property |
|
|
||||||
|
|
||||||
## Multi-Solution Detection
|
|
||||||
|
|
||||||
If user needs BOTH:
|
|
||||||
- Static results (stress, displacement)
|
|
||||||
- Modal results (frequency)
|
|
||||||
|
|
||||||
Then set `solution_name=None` to solve ALL solutions.
|
|
||||||
|
|
||||||
## Validation Before Action
|
|
||||||
|
|
||||||
Always validate before:
|
|
||||||
- Starting optimization (config validator)
|
|
||||||
- Generating files (check paths exist)
|
|
||||||
- Running FEA (check NX files present)
|
|
||||||
|
|
||||||
## Dashboard Integration
|
|
||||||
|
|
||||||
- Setup/Config: **You handle it**
|
|
||||||
- Real-time monitoring: **Dashboard at localhost:3000**
|
|
||||||
- Results analysis: **Both (you interpret, dashboard visualizes)**
|
|
||||||
|
|
||||||
## CRITICAL: Code Reuse Protocol (MUST FOLLOW)
|
|
||||||
|
|
||||||
### STOP! Before Writing ANY Code in run_optimization.py
|
|
||||||
|
|
||||||
**This is the #1 cause of code duplication. EVERY TIME you're about to write:**
|
|
||||||
- A function longer than 20 lines
|
|
||||||
- Any physics/math calculations (Zernike, RMS, stress, etc.)
|
|
||||||
- Any OP2/BDF parsing logic
|
|
||||||
- Any post-processing or extraction logic
|
|
||||||
|
|
||||||
**STOP and run this checklist:**
|
|
||||||
|
|
||||||
|
**Example workflow for NX journal development:**
|
||||||
```
|
```
|
||||||
□ Did I check optimization_engine/extractors/__init__.py?
|
1. User: "Extract mass from NX part"
|
||||||
□ Did I grep for similar function names in optimization_engine/?
|
2. Claude: Query nxopen_get_class("Part") to find mass-related methods
|
||||||
□ Does this functionality exist somewhere else in the codebase?
|
3. Claude: Query nxopen_get_class("Session") to understand part access
|
||||||
|
4. Claude: Check existing extractors for similar functionality
|
||||||
|
5. Claude: Write code using verified API calls
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**MCP Server Setup:** See `mcp-server/README.md`
|
||||||
|
|
||||||
|
## CRITICAL: Code Reuse Protocol
|
||||||
|
|
||||||
### The 20-Line Rule
|
### The 20-Line Rule
|
||||||
|
|
||||||
If you're writing a function longer than ~20 lines in `studies/*/run_optimization.py`:
|
If you're writing a function longer than ~20 lines in `run_optimization.py`:
|
||||||
1. **STOP** - This is a code smell
|
1. **STOP** - This is a code smell
|
||||||
2. **SEARCH** - The functionality probably exists
|
2. **SEARCH** - Check `optimization_engine/extractors/`
|
||||||
3. **IMPORT** - Use the existing module
|
3. **IMPORT** - Use existing extractor
|
||||||
4. **Only if truly new** - Create in `optimization_engine/extractors/`, NOT in the study
|
4. **Only if truly new** - Follow EXT_01 to create new extractor
|
||||||
|
|
||||||
### Available Extractors (ALWAYS CHECK FIRST)
|
### Available Extractors
|
||||||
|
|
||||||
| Module | Functions | Use For |
|
| ID | Physics | Function |
|
||||||
|--------|-----------|---------|
|
|----|---------|----------|
|
||||||
| **`extract_zernike.py`** | `ZernikeExtractor`, `extract_zernike_from_op2`, `extract_zernike_filtered_rms`, `extract_zernike_relative_rms` | Telescope mirror WFE analysis - Noll indexing, RMS calculations, multi-subcase |
|
| E1 | Displacement | `extract_displacement()` |
|
||||||
| **`zernike_helpers.py`** | `create_zernike_objective`, `ZernikeObjectiveBuilder`, `extract_zernike_for_trial` | Zernike optimization integration |
|
| E2 | Frequency | `extract_frequency()` |
|
||||||
| **`extract_displacement.py`** | `extract_displacement` | Max/min displacement from OP2 |
|
| E3 | Stress | `extract_solid_stress()` |
|
||||||
| **`extract_von_mises_stress.py`** | `extract_solid_stress` | Von Mises stress extraction |
|
| E4 | BDF Mass | `extract_mass_from_bdf()` |
|
||||||
| **`extract_frequency.py`** | `extract_frequency` | Natural frequencies from OP2 |
|
| E5 | CAD Mass | `extract_mass_from_expression()` |
|
||||||
| **`extract_mass.py`** | `extract_mass_from_expression` | CAD mass property |
|
| E8-10 | Zernike | `extract_zernike_*()` |
|
||||||
| **`op2_extractor.py`** | Generic OP2 result extraction | Low-level OP2 access |
|
|
||||||
| **`field_data_extractor.py`** | Field data for neural networks | Training data generation |
|
|
||||||
|
|
||||||
### Correct Pattern: Zernike Example
|
**Full catalog**: `docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md`
|
||||||
|
|
||||||
**❌ WRONG - What I did (and must NEVER do again):**
|
## Privilege Levels
|
||||||
```python
|
|
||||||
# studies/m1_mirror/run_optimization.py
|
|
||||||
def noll_indices(j): # 30 lines
|
|
||||||
...
|
|
||||||
def zernike_radial(n, m, r): # 20 lines
|
|
||||||
...
|
|
||||||
def compute_zernike_coefficients(...): # 80 lines
|
|
||||||
...
|
|
||||||
def compute_rms_metrics(...): # 40 lines
|
|
||||||
...
|
|
||||||
# Total: 500+ lines of duplicated code
|
|
||||||
```
|
|
||||||
|
|
||||||
**✅ CORRECT - What I should have done:**
|
| Level | Operations | Extensions |
|
||||||
```python
|
|-------|------------|------------|
|
||||||
# studies/m1_mirror/run_optimization.py
|
| **user** | All OP_* | None |
|
||||||
from optimization_engine.extractors import (
|
| **power_user** | All OP_* | EXT_01, EXT_02 |
|
||||||
ZernikeExtractor,
|
| **admin** | All | All |
|
||||||
extract_zernike_for_trial
|
|
||||||
)
|
|
||||||
|
|
||||||
# In objective function - 5 lines instead of 500
|
Default to `user` unless explicitly stated otherwise.
|
||||||
extractor = ZernikeExtractor(op2_file, bdf_file)
|
|
||||||
result = extractor.extract_relative(target_subcase="40", reference_subcase="20")
|
|
||||||
filtered_rms = result['relative_filtered_rms_nm']
|
|
||||||
```
|
|
||||||
|
|
||||||
### Creating New Extractors (Only When Truly Needed)
|
|
||||||
|
|
||||||
When functionality genuinely doesn't exist:
|
|
||||||
|
|
||||||
```
|
|
||||||
1. CREATE module in optimization_engine/extractors/new_feature.py
|
|
||||||
2. ADD exports to optimization_engine/extractors/__init__.py
|
|
||||||
3. UPDATE this table in CLAUDE.md
|
|
||||||
4. IMPORT in run_optimization.py (just the import, not the implementation)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why This Is Critical
|
|
||||||
|
|
||||||
| Embedding Code in Studies | Using Central Extractors |
|
|
||||||
|---------------------------|-------------------------|
|
|
||||||
| Bug fixes don't propagate | Fix once, applies everywhere |
|
|
||||||
| No unit tests | Tested in isolation |
|
|
||||||
| Hard to discover | Clear API in __init__.py |
|
|
||||||
| Copy-paste errors | Single source of truth |
|
|
||||||
| 500+ line studies | Clean, readable studies |
|
|
||||||
|
|
||||||
## Key Principles
|
## Key Principles
|
||||||
|
|
||||||
1. **Conversation first** - Don't ask user to edit JSON manually
|
1. **Conversation first** - Don't ask user to edit JSON manually
|
||||||
2. **Validate everything** - Catch errors before they cause failures
|
2. **Validate everything** - Catch errors before they cause failures
|
||||||
3. **Explain decisions** - Say why you chose a sampler/protocol
|
3. **Explain decisions** - Say why you chose a sampler/protocol
|
||||||
4. **Sensible defaults** - User only specifies what they care about
|
4. **NEVER modify master files** - Copy NX files to study directory
|
||||||
5. **Progressive disclosure** - Start simple, add complexity when needed
|
5. **ALWAYS reuse code** - Check extractors before writing new code
|
||||||
6. **NEVER modify master files** - Always copy model files to study working directory before optimization. User's source files must remain untouched. If corruption occurs during iteration, working copy can be deleted and re-copied.
|
|
||||||
7. **ALWAYS reuse existing code** - Check `optimization_engine/extractors/` BEFORE writing any new post-processing logic. Never duplicate functionality that already exists.
|
|
||||||
|
|
||||||
## Current State Awareness
|
## CRITICAL: NX FEM Mesh Update Requirements
|
||||||
|
|
||||||
Check these before suggesting actions:
|
**When parametric optimization produces identical results, the mesh is NOT updating!**
|
||||||
- Running background processes: `/tasks` command
|
|
||||||
- Study databases: `studies/*/2_results/study.db`
|
### Required File Chain
|
||||||
- Model files: `studies/*/1_setup/model/`
|
```
|
||||||
- Dashboard status: Check if servers running
|
.sim (Simulation)
|
||||||
|
└── .fem (FEM)
|
||||||
|
└── *_i.prt (Idealized Part) ← MUST EXIST AND BE LOADED!
|
||||||
|
└── .prt (Geometry Part)
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Fix (Already Implemented in solve_simulation.py)
|
||||||
|
The idealized part (`*_i.prt`) MUST be explicitly loaded BEFORE calling `UpdateFemodel()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# STEP 2: Load idealized part first (CRITICAL!)
|
||||||
|
for filename in os.listdir(working_dir):
|
||||||
|
if '_i.prt' in filename.lower():
|
||||||
|
idealized_part, status = theSession.Parts.Open(path)
|
||||||
|
break
|
||||||
|
|
||||||
|
# THEN update FEM - now it will actually regenerate the mesh
|
||||||
|
feModel.UpdateFemodel()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Without loading the `_i.prt`, `UpdateFemodel()` runs but the mesh doesn't change!**
|
||||||
|
|
||||||
|
### Study Setup Checklist
|
||||||
|
When creating a new study, ensure ALL these files are copied:
|
||||||
|
- [ ] `Model.prt` - Geometry part
|
||||||
|
- [ ] `Model_fem1_i.prt` - Idealized part ← **OFTEN MISSING!**
|
||||||
|
- [ ] `Model_fem1.fem` - FEM file
|
||||||
|
- [ ] `Model_sim1.sim` - Simulation file
|
||||||
|
|
||||||
|
See `docs/protocols/operations/OP_06_TROUBLESHOOT.md` for full troubleshooting guide.
|
||||||
|
|
||||||
|
## Developer Documentation
|
||||||
|
|
||||||
|
**For developers maintaining Atomizer**:
|
||||||
|
- Read `.claude/skills/DEV_DOCUMENTATION.md`
|
||||||
|
- Use self-documenting commands: "Document the {feature} I added"
|
||||||
|
- Commit code + docs together
|
||||||
|
|
||||||
## When Uncertain
|
## When Uncertain
|
||||||
|
|
||||||
1. Read the relevant skill file
|
1. Check `.claude/skills/00_BOOTSTRAP.md` for task routing
|
||||||
2. Check docs/06_PROTOCOLS_DETAILED/
|
2. Check `.claude/skills/01_CHEATSHEET.md` for quick lookup
|
||||||
3. Look at existing similar studies
|
3. Load relevant protocol from `docs/protocols/`
|
||||||
4. Ask user for clarification
|
4. Ask user for clarification
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import os
|
|||||||
|
|
||||||
# NX Installation Directory
|
# NX Installation Directory
|
||||||
# Change this to update NX version across entire Atomizer codebase
|
# Change this to update NX version across entire Atomizer codebase
|
||||||
NX_VERSION = "2412"
|
NX_VERSION = "2506"
|
||||||
NX_INSTALLATION_DIR = Path(f"C:/Program Files/Siemens/NX{NX_VERSION}")
|
NX_INSTALLATION_DIR = Path(f"C:/Program Files/Siemens/NX{NX_VERSION}")
|
||||||
|
|
||||||
# Derived NX Paths (automatically updated when NX_VERSION changes)
|
# Derived NX Paths (automatically updated when NX_VERSION changes)
|
||||||
|
|||||||
160
docs/protocols/README.md
Normal file
160
docs/protocols/README.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Atomizer Protocol Operating System (POS)
|
||||||
|
|
||||||
|
**Version**: 1.0
|
||||||
|
**Last Updated**: 2025-12-05
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This directory contains the **Protocol Operating System (POS)** - a 4-layer documentation architecture optimized for LLM consumption.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
protocols/
|
||||||
|
├── README.md # This file
|
||||||
|
├── operations/ # Layer 2: How-to guides
|
||||||
|
│ ├── OP_01_CREATE_STUDY.md
|
||||||
|
│ ├── OP_02_RUN_OPTIMIZATION.md
|
||||||
|
│ ├── OP_03_MONITOR_PROGRESS.md
|
||||||
|
│ ├── OP_04_ANALYZE_RESULTS.md
|
||||||
|
│ ├── OP_05_EXPORT_TRAINING_DATA.md
|
||||||
|
│ └── OP_06_TROUBLESHOOT.md
|
||||||
|
├── system/ # Layer 3: Core specifications
|
||||||
|
│ ├── SYS_10_IMSO.md
|
||||||
|
│ ├── SYS_11_MULTI_OBJECTIVE.md
|
||||||
|
│ ├── SYS_12_EXTRACTOR_LIBRARY.md
|
||||||
|
│ ├── SYS_13_DASHBOARD_TRACKING.md
|
||||||
|
│ └── SYS_14_NEURAL_ACCELERATION.md
|
||||||
|
└── extensions/ # Layer 4: Extensibility guides
|
||||||
|
├── EXT_01_CREATE_EXTRACTOR.md
|
||||||
|
├── EXT_02_CREATE_HOOK.md
|
||||||
|
├── EXT_03_CREATE_PROTOCOL.md
|
||||||
|
├── EXT_04_CREATE_SKILL.md
|
||||||
|
└── templates/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer Descriptions
|
||||||
|
|
||||||
|
### Layer 1: Bootstrap (`.claude/skills/`)
|
||||||
|
Entry point for LLM sessions. Contains:
|
||||||
|
- `00_BOOTSTRAP.md` - Quick orientation and task routing
|
||||||
|
- `01_CHEATSHEET.md` - "I want X → Use Y" lookup
|
||||||
|
- `02_CONTEXT_LOADER.md` - What to load per task
|
||||||
|
- `PROTOCOL_EXECUTION.md` - Meta-protocol for execution
|
||||||
|
|
||||||
|
### Layer 2: Operations (`operations/`)
|
||||||
|
Day-to-day how-to guides:
|
||||||
|
- **OP_01**: Create optimization study
|
||||||
|
- **OP_02**: Run optimization
|
||||||
|
- **OP_03**: Monitor progress
|
||||||
|
- **OP_04**: Analyze results
|
||||||
|
- **OP_05**: Export training data
|
||||||
|
- **OP_06**: Troubleshoot issues
|
||||||
|
|
||||||
|
### Layer 3: System (`system/`)
|
||||||
|
Core technical specifications:
|
||||||
|
- **SYS_10**: Intelligent Multi-Strategy Optimization (IMSO)
|
||||||
|
- **SYS_11**: Multi-Objective Support (MANDATORY)
|
||||||
|
- **SYS_12**: Extractor Library
|
||||||
|
- **SYS_13**: Real-Time Dashboard Tracking
|
||||||
|
- **SYS_14**: Neural Network Acceleration
|
||||||
|
|
||||||
|
### Layer 4: Extensions (`extensions/`)
|
||||||
|
Guides for extending Atomizer:
|
||||||
|
- **EXT_01**: Create new extractor
|
||||||
|
- **EXT_02**: Create lifecycle hook
|
||||||
|
- **EXT_03**: Create new protocol
|
||||||
|
- **EXT_04**: Create new skill
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Template
|
||||||
|
|
||||||
|
All protocols follow this structure:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# {LAYER}_{NUMBER}_{NAME}.md
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: {Full Name}
|
||||||
|
LAYER: {Operations|System|Extensions}
|
||||||
|
VERSION: {Major.Minor}
|
||||||
|
STATUS: {Active|Draft|Deprecated}
|
||||||
|
LAST_UPDATED: {YYYY-MM-DD}
|
||||||
|
PRIVILEGE: {user|power_user|admin}
|
||||||
|
LOAD_WITH: [{dependencies}]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
{1-3 sentence description}
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
{Tables, key parameters}
|
||||||
|
|
||||||
|
## Detailed Specification
|
||||||
|
{Full content}
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
{Working examples}
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
- Depends On: []
|
||||||
|
- Used By: []
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Navigation
|
||||||
|
|
||||||
|
### By Task
|
||||||
|
|
||||||
|
| I want to... | Protocol |
|
||||||
|
|--------------|----------|
|
||||||
|
| Create a study | [OP_01](operations/OP_01_CREATE_STUDY.md) |
|
||||||
|
| Run optimization | [OP_02](operations/OP_02_RUN_OPTIMIZATION.md) |
|
||||||
|
| Check progress | [OP_03](operations/OP_03_MONITOR_PROGRESS.md) |
|
||||||
|
| Analyze results | [OP_04](operations/OP_04_ANALYZE_RESULTS.md) |
|
||||||
|
| Export neural data | [OP_05](operations/OP_05_EXPORT_TRAINING_DATA.md) |
|
||||||
|
| Fix errors | [OP_06](operations/OP_06_TROUBLESHOOT.md) |
|
||||||
|
| Add extractor | [EXT_01](extensions/EXT_01_CREATE_EXTRACTOR.md) |
|
||||||
|
|
||||||
|
### By Protocol Number
|
||||||
|
|
||||||
|
| # | Name | Layer |
|
||||||
|
|---|------|-------|
|
||||||
|
| 10 | IMSO | [System](system/SYS_10_IMSO.md) |
|
||||||
|
| 11 | Multi-Objective | [System](system/SYS_11_MULTI_OBJECTIVE.md) |
|
||||||
|
| 12 | Extractors | [System](system/SYS_12_EXTRACTOR_LIBRARY.md) |
|
||||||
|
| 13 | Dashboard | [System](system/SYS_13_DASHBOARD_TRACKING.md) |
|
||||||
|
| 14 | Neural | [System](system/SYS_14_NEURAL_ACCELERATION.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Privilege Levels
|
||||||
|
|
||||||
|
| Level | Operations | System | Extensions |
|
||||||
|
|-------|------------|--------|------------|
|
||||||
|
| user | All OP_* | Read SYS_* | None |
|
||||||
|
| power_user | All OP_* | Read SYS_* | EXT_01, EXT_02 |
|
||||||
|
| admin | All | All | All |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial Protocol Operating System |
|
||||||
395
docs/protocols/extensions/EXT_01_CREATE_EXTRACTOR.md
Normal file
395
docs/protocols/extensions/EXT_01_CREATE_EXTRACTOR.md
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
# EXT_01: Create New Extractor
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Create New Physics Extractor
|
||||||
|
LAYER: Extensions
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: power_user
|
||||||
|
LOAD_WITH: [SYS_12_EXTRACTOR_LIBRARY]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This protocol guides you through creating a new physics extractor for the centralized extractor library. Follow this when you need to extract results not covered by existing extractors.
|
||||||
|
|
||||||
|
**Privilege Required**: power_user or admin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| Need physics not in library | Follow this protocol |
|
||||||
|
| "create extractor", "new extractor" | Follow this protocol |
|
||||||
|
| Custom result extraction needed | Follow this protocol |
|
||||||
|
|
||||||
|
**First**: Check [SYS_12_EXTRACTOR_LIBRARY](../system/SYS_12_EXTRACTOR_LIBRARY.md) - the functionality may already exist!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Create in**: `optimization_engine/extractors/`
|
||||||
|
**Export from**: `optimization_engine/extractors/__init__.py`
|
||||||
|
**Document in**: Update SYS_12 and this protocol
|
||||||
|
|
||||||
|
**Template location**: `docs/protocols/extensions/templates/extractor_template.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step Guide
|
||||||
|
|
||||||
|
### Step 1: Verify Need
|
||||||
|
|
||||||
|
Before creating:
|
||||||
|
1. Check existing extractors in [SYS_12](../system/SYS_12_EXTRACTOR_LIBRARY.md)
|
||||||
|
2. Search codebase: `grep -r "your_physics" optimization_engine/`
|
||||||
|
3. Confirm no existing solution
|
||||||
|
|
||||||
|
### Step 1.5: Research NX Open APIs (REQUIRED for NX extractors)
|
||||||
|
|
||||||
|
**If the extractor needs NX Open APIs** (not just pyNastran OP2 parsing):
|
||||||
|
|
||||||
|
```
|
||||||
|
# 1. Search for relevant NX Open APIs
|
||||||
|
siemens_docs_search("inertia properties NXOpen")
|
||||||
|
siemens_docs_search("mass properties body NXOpen.CAE")
|
||||||
|
|
||||||
|
# 2. Fetch detailed documentation for promising classes
|
||||||
|
siemens_docs_fetch("NXOpen.MeasureManager")
|
||||||
|
siemens_docs_fetch("NXOpen.UF.UFWeight")
|
||||||
|
|
||||||
|
# 3. Get method signatures
|
||||||
|
siemens_docs_search("AskMassProperties NXOpen")
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use NX Open vs pyNastran:**
|
||||||
|
|
||||||
|
| Data Source | Tool | Example |
|
||||||
|
|-------------|------|---------|
|
||||||
|
| OP2 results (stress, disp, freq) | pyNastran | `extract_displacement()` |
|
||||||
|
| CAD properties (mass, inertia) | NX Open | New extractor with NXOpen API |
|
||||||
|
| BDF data (mesh, properties) | pyNastran | `extract_mass_from_bdf()` |
|
||||||
|
| NX expressions | NX Open | `extract_mass_from_expression()` |
|
||||||
|
| FEM model data | NX Open CAE | Needs `NXOpen.CAE.*` APIs |
|
||||||
|
|
||||||
|
**Document the APIs used** in the extractor docstring:
|
||||||
|
```python
|
||||||
|
def extract_inertia(part_file: Path) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract mass and inertia properties from NX part.
|
||||||
|
|
||||||
|
NX Open APIs Used:
|
||||||
|
- NXOpen.MeasureManager.NewMassProperties()
|
||||||
|
- NXOpen.MeasureBodies.InformationUnit
|
||||||
|
- NXOpen.UF.UFWeight.AskProps()
|
||||||
|
|
||||||
|
See: docs.sw.siemens.com for full API reference
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create Extractor File
|
||||||
|
|
||||||
|
Create `optimization_engine/extractors/extract_{physics}.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Extract {Physics Name} from FEA results.
|
||||||
|
|
||||||
|
Author: {Your Name}
|
||||||
|
Created: {Date}
|
||||||
|
Version: 1.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional, Union
|
||||||
|
from pyNastran.op2.op2 import OP2
|
||||||
|
|
||||||
|
|
||||||
|
def extract_{physics}(
|
||||||
|
op2_file: Union[str, Path],
|
||||||
|
subcase: int = 1,
|
||||||
|
# Add other parameters as needed
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract {physics description} from OP2 file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
op2_file: Path to the OP2 results file
|
||||||
|
subcase: Subcase number to extract (default: 1)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- '{main_result}': The primary result value
|
||||||
|
- '{secondary}': Additional result info
|
||||||
|
- 'subcase': The subcase extracted
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If OP2 file doesn't exist
|
||||||
|
KeyError: If subcase not found in results
|
||||||
|
ValueError: If result data is invalid
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = extract_{physics}('model.op2', subcase=1)
|
||||||
|
>>> print(result['{main_result}'])
|
||||||
|
123.45
|
||||||
|
"""
|
||||||
|
op2_file = Path(op2_file)
|
||||||
|
|
||||||
|
if not op2_file.exists():
|
||||||
|
raise FileNotFoundError(f"OP2 file not found: {op2_file}")
|
||||||
|
|
||||||
|
# Read OP2 file
|
||||||
|
op2 = OP2()
|
||||||
|
op2.read_op2(str(op2_file))
|
||||||
|
|
||||||
|
# Extract your physics
|
||||||
|
# TODO: Implement extraction logic
|
||||||
|
|
||||||
|
# Example for displacement-like result:
|
||||||
|
if subcase not in op2.displacements:
|
||||||
|
raise KeyError(f"Subcase {subcase} not found in results")
|
||||||
|
|
||||||
|
data = op2.displacements[subcase]
|
||||||
|
# Process data...
|
||||||
|
|
||||||
|
return {
|
||||||
|
'{main_result}': computed_value,
|
||||||
|
'{secondary}': secondary_value,
|
||||||
|
'subcase': subcase,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Optional: Class-based extractor for complex cases
|
||||||
|
class {Physics}Extractor:
|
||||||
|
"""
|
||||||
|
Class-based extractor for {physics} with state management.
|
||||||
|
|
||||||
|
Use when extraction requires multiple steps or configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, op2_file: Union[str, Path], **config):
|
||||||
|
self.op2_file = Path(op2_file)
|
||||||
|
self.config = config
|
||||||
|
self._op2 = None
|
||||||
|
|
||||||
|
def _load_op2(self):
|
||||||
|
"""Lazy load OP2 file."""
|
||||||
|
if self._op2 is None:
|
||||||
|
self._op2 = OP2()
|
||||||
|
self._op2.read_op2(str(self.op2_file))
|
||||||
|
return self._op2
|
||||||
|
|
||||||
|
def extract(self, subcase: int = 1) -> Dict[str, Any]:
|
||||||
|
"""Extract results for given subcase."""
|
||||||
|
op2 = self._load_op2()
|
||||||
|
# Implementation here
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Add to __init__.py
|
||||||
|
|
||||||
|
Edit `optimization_engine/extractors/__init__.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add import
|
||||||
|
from .extract_{physics} import extract_{physics}
|
||||||
|
# Or for class
|
||||||
|
from .extract_{physics} import {Physics}Extractor
|
||||||
|
|
||||||
|
# Add to __all__
|
||||||
|
__all__ = [
|
||||||
|
# ... existing exports ...
|
||||||
|
'extract_{physics}',
|
||||||
|
'{Physics}Extractor',
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Write Tests
|
||||||
|
|
||||||
|
Create `tests/test_extract_{physics}.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Tests for {physics} extractor."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
from optimization_engine.extractors import extract_{physics}
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtract{Physics}:
|
||||||
|
"""Test suite for {physics} extraction."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_op2(self, tmp_path):
|
||||||
|
"""Create or copy sample OP2 for testing."""
|
||||||
|
# Either copy existing test file or create mock
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_basic_extraction(self, sample_op2):
|
||||||
|
"""Test basic extraction works."""
|
||||||
|
result = extract_{physics}(sample_op2)
|
||||||
|
assert '{main_result}' in result
|
||||||
|
assert isinstance(result['{main_result}'], float)
|
||||||
|
|
||||||
|
def test_file_not_found(self):
|
||||||
|
"""Test error handling for missing file."""
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
extract_{physics}('nonexistent.op2')
|
||||||
|
|
||||||
|
def test_invalid_subcase(self, sample_op2):
|
||||||
|
"""Test error handling for invalid subcase."""
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
extract_{physics}(sample_op2, subcase=999)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Document
|
||||||
|
|
||||||
|
#### Update SYS_12_EXTRACTOR_LIBRARY.md
|
||||||
|
|
||||||
|
Add to Quick Reference table:
|
||||||
|
```markdown
|
||||||
|
| E{N} | {Physics} | `extract_{physics}()` | .op2 | {unit} |
|
||||||
|
```
|
||||||
|
|
||||||
|
Add detailed section:
|
||||||
|
```markdown
|
||||||
|
### E{N}: {Physics} Extraction
|
||||||
|
|
||||||
|
**Module**: `optimization_engine.extractors.extract_{physics}`
|
||||||
|
|
||||||
|
\`\`\`python
|
||||||
|
from optimization_engine.extractors import extract_{physics}
|
||||||
|
|
||||||
|
result = extract_{physics}(op2_file, subcase=1)
|
||||||
|
{main_result} = result['{main_result}']
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update skills/modules/extractors-catalog.md
|
||||||
|
|
||||||
|
Add entry following existing pattern.
|
||||||
|
|
||||||
|
### Step 6: Validate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run tests
|
||||||
|
pytest tests/test_extract_{physics}.py -v
|
||||||
|
|
||||||
|
# Test import
|
||||||
|
python -c "from optimization_engine.extractors import extract_{physics}; print('OK')"
|
||||||
|
|
||||||
|
# Test with real file
|
||||||
|
python -c "
|
||||||
|
from optimization_engine.extractors import extract_{physics}
|
||||||
|
result = extract_{physics}('path/to/test.op2')
|
||||||
|
print(result)
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Extractor Design Guidelines
|
||||||
|
|
||||||
|
### Do's
|
||||||
|
|
||||||
|
- Return dictionaries with clear keys
|
||||||
|
- Include metadata (subcase, units, etc.)
|
||||||
|
- Handle edge cases gracefully
|
||||||
|
- Provide clear error messages
|
||||||
|
- Document all parameters and returns
|
||||||
|
- Write tests
|
||||||
|
|
||||||
|
### Don'ts
|
||||||
|
|
||||||
|
- Don't re-parse OP2 multiple times in one call
|
||||||
|
- Don't hardcode paths
|
||||||
|
- Don't swallow exceptions silently
|
||||||
|
- Don't return raw pyNastran objects
|
||||||
|
- Don't modify input files
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
| Type | Convention | Example |
|
||||||
|
|------|------------|---------|
|
||||||
|
| File | `extract_{physics}.py` | `extract_thermal.py` |
|
||||||
|
| Function | `extract_{physics}` | `extract_thermal` |
|
||||||
|
| Class | `{Physics}Extractor` | `ThermalExtractor` |
|
||||||
|
| Return key | lowercase_with_underscores | `max_temperature` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example: Thermal Gradient Extractor
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Extract thermal gradients from temperature results."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any
|
||||||
|
from pyNastran.op2.op2 import OP2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def extract_thermal_gradient(
|
||||||
|
op2_file: Path,
|
||||||
|
subcase: int = 1,
|
||||||
|
direction: str = 'magnitude'
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract thermal gradient from temperature field.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
op2_file: Path to OP2 file
|
||||||
|
subcase: Subcase number
|
||||||
|
direction: 'magnitude', 'x', 'y', or 'z'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with gradient results
|
||||||
|
"""
|
||||||
|
op2 = OP2()
|
||||||
|
op2.read_op2(str(op2_file))
|
||||||
|
|
||||||
|
temps = op2.temperatures[subcase]
|
||||||
|
# Calculate gradient...
|
||||||
|
|
||||||
|
return {
|
||||||
|
'max_gradient': max_grad,
|
||||||
|
'mean_gradient': mean_grad,
|
||||||
|
'max_gradient_location': location,
|
||||||
|
'direction': direction,
|
||||||
|
'subcase': subcase,
|
||||||
|
'unit': 'K/mm'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| Import error | Not added to __init__.py | Add export |
|
||||||
|
| "No module" | Wrong file location | Check path |
|
||||||
|
| KeyError | Wrong OP2 data structure | Debug OP2 contents |
|
||||||
|
| Tests fail | Missing test data | Create fixtures |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Reference**: [SYS_12_EXTRACTOR_LIBRARY](../system/SYS_12_EXTRACTOR_LIBRARY.md)
|
||||||
|
- **Template**: `templates/extractor_template.py`
|
||||||
|
- **Related**: [EXT_02_CREATE_HOOK](./EXT_02_CREATE_HOOK.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial release |
|
||||||
366
docs/protocols/extensions/EXT_02_CREATE_HOOK.md
Normal file
366
docs/protocols/extensions/EXT_02_CREATE_HOOK.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# EXT_02: Create Lifecycle Hook
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Create Lifecycle Hook Plugin
|
||||||
|
LAYER: Extensions
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: power_user
|
||||||
|
LOAD_WITH: []
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This protocol guides you through creating lifecycle hooks that execute at specific points during optimization. Hooks enable custom logic injection without modifying core code.
|
||||||
|
|
||||||
|
**Privilege Required**: power_user or admin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| Need custom logic at specific point | Follow this protocol |
|
||||||
|
| "create hook", "callback" | Follow this protocol |
|
||||||
|
| Want to log/validate/modify at runtime | Follow this protocol |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Hook Points Available**:
|
||||||
|
|
||||||
|
| Hook Point | When It Runs | Use Case |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `pre_mesh` | Before meshing | Validate geometry |
|
||||||
|
| `post_mesh` | After meshing | Check mesh quality |
|
||||||
|
| `pre_solve` | Before solver | Log trial start |
|
||||||
|
| `post_solve` | After solver | Validate results |
|
||||||
|
| `post_extraction` | After extraction | Custom metrics |
|
||||||
|
| `post_calculation` | After objectives | Derived quantities |
|
||||||
|
| `custom_objective` | Custom objective | Complex objectives |
|
||||||
|
|
||||||
|
**Create in**: `optimization_engine/plugins/{hook_point}/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step Guide
|
||||||
|
|
||||||
|
### Step 1: Identify Hook Point
|
||||||
|
|
||||||
|
Choose the appropriate hook point:
|
||||||
|
|
||||||
|
```
|
||||||
|
Trial Flow:
|
||||||
|
│
|
||||||
|
├─► PRE_MESH → Validate model before meshing
|
||||||
|
│
|
||||||
|
├─► POST_MESH → Check mesh quality
|
||||||
|
│
|
||||||
|
├─► PRE_SOLVE → Log trial start, validate inputs
|
||||||
|
│
|
||||||
|
├─► POST_SOLVE → Check solve success, capture timing
|
||||||
|
│
|
||||||
|
├─► POST_EXTRACTION → Compute derived quantities
|
||||||
|
│
|
||||||
|
├─► POST_CALCULATION → Final validation, logging
|
||||||
|
│
|
||||||
|
└─► CUSTOM_OBJECTIVE → Custom objective functions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create Hook File
|
||||||
|
|
||||||
|
Create `optimization_engine/plugins/{hook_point}/{hook_name}.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
{Hook Description}
|
||||||
|
|
||||||
|
Author: {Your Name}
|
||||||
|
Created: {Date}
|
||||||
|
Version: 1.0
|
||||||
|
Hook Point: {hook_point}
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
def {hook_name}_hook(context: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
{Description of what this hook does}.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Dictionary containing:
|
||||||
|
- trial_number: Current trial number
|
||||||
|
- design_params: Current design parameters
|
||||||
|
- results: Results so far (if post-extraction)
|
||||||
|
- config: Optimization config
|
||||||
|
- working_dir: Path to working directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with computed values or modifications.
|
||||||
|
Return empty dict if no modifications needed.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = {hook_name}_hook({'trial_number': 1, ...})
|
||||||
|
>>> print(result)
|
||||||
|
{'{computed_key}': 123.45}
|
||||||
|
"""
|
||||||
|
# Access context
|
||||||
|
trial_num = context.get('trial_number')
|
||||||
|
design_params = context.get('design_params', {})
|
||||||
|
results = context.get('results', {})
|
||||||
|
|
||||||
|
# Your logic here
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# Return computed values
|
||||||
|
return {
|
||||||
|
'{computed_key}': computed_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register_hooks(hook_manager):
|
||||||
|
"""
|
||||||
|
Register this hook with the hook manager.
|
||||||
|
|
||||||
|
This function is called automatically when plugins are loaded.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hook_manager: The HookManager instance
|
||||||
|
"""
|
||||||
|
hook_manager.register_hook(
|
||||||
|
hook_point='{hook_point}',
|
||||||
|
function={hook_name}_hook,
|
||||||
|
name='{hook_name}_hook',
|
||||||
|
description='{Brief description}',
|
||||||
|
priority=100, # Lower = runs earlier
|
||||||
|
enabled=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Test Hook
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Test in isolation
|
||||||
|
from optimization_engine.plugins.{hook_point}.{hook_name} import {hook_name}_hook
|
||||||
|
|
||||||
|
test_context = {
|
||||||
|
'trial_number': 1,
|
||||||
|
'design_params': {'thickness': 5.0},
|
||||||
|
'results': {'max_stress': 200.0},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {hook_name}_hook(test_context)
|
||||||
|
print(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Enable Hook
|
||||||
|
|
||||||
|
Hooks are auto-discovered from the plugins directory. To verify:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.plugins.hook_manager import HookManager
|
||||||
|
|
||||||
|
manager = HookManager()
|
||||||
|
manager.discover_plugins()
|
||||||
|
print(manager.list_hooks())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hook Examples
|
||||||
|
|
||||||
|
### Example 1: Safety Factor Calculator (post_calculation)
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Calculate safety factor after stress extraction."""
|
||||||
|
|
||||||
|
def safety_factor_hook(context):
|
||||||
|
"""Calculate safety factor from stress results."""
|
||||||
|
results = context.get('results', {})
|
||||||
|
config = context.get('config', {})
|
||||||
|
|
||||||
|
max_stress = results.get('max_von_mises', 0)
|
||||||
|
yield_strength = config.get('material', {}).get('yield_strength', 250)
|
||||||
|
|
||||||
|
if max_stress > 0:
|
||||||
|
safety_factor = yield_strength / max_stress
|
||||||
|
else:
|
||||||
|
safety_factor = float('inf')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'safety_factor': safety_factor,
|
||||||
|
'yield_strength': yield_strength,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register_hooks(hook_manager):
|
||||||
|
hook_manager.register_hook(
|
||||||
|
hook_point='post_calculation',
|
||||||
|
function=safety_factor_hook,
|
||||||
|
name='safety_factor_hook',
|
||||||
|
description='Calculate safety factor from stress',
|
||||||
|
priority=100,
|
||||||
|
enabled=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Trial Logger (pre_solve)
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Log trial information before solve."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def trial_logger_hook(context):
|
||||||
|
"""Log trial start information."""
|
||||||
|
trial_num = context.get('trial_number')
|
||||||
|
design_params = context.get('design_params', {})
|
||||||
|
working_dir = context.get('working_dir', Path('.'))
|
||||||
|
|
||||||
|
log_entry = {
|
||||||
|
'trial': trial_num,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'params': design_params,
|
||||||
|
}
|
||||||
|
|
||||||
|
log_file = working_dir / 'trial_log.jsonl'
|
||||||
|
with open(log_file, 'a') as f:
|
||||||
|
f.write(json.dumps(log_entry) + '\n')
|
||||||
|
|
||||||
|
return {} # No modifications
|
||||||
|
|
||||||
|
|
||||||
|
def register_hooks(hook_manager):
|
||||||
|
hook_manager.register_hook(
|
||||||
|
hook_point='pre_solve',
|
||||||
|
function=trial_logger_hook,
|
||||||
|
name='trial_logger_hook',
|
||||||
|
description='Log trial parameters before solve',
|
||||||
|
priority=10, # Run early
|
||||||
|
enabled=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Mesh Quality Check (post_mesh)
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Validate mesh quality after meshing."""
|
||||||
|
|
||||||
|
|
||||||
|
def mesh_quality_hook(context):
|
||||||
|
"""Check mesh quality metrics."""
|
||||||
|
mesh_file = context.get('mesh_file')
|
||||||
|
|
||||||
|
# Check quality metrics
|
||||||
|
quality_issues = []
|
||||||
|
|
||||||
|
# ... quality checks ...
|
||||||
|
|
||||||
|
if quality_issues:
|
||||||
|
context['warnings'] = context.get('warnings', []) + quality_issues
|
||||||
|
|
||||||
|
return {
|
||||||
|
'mesh_quality_passed': len(quality_issues) == 0,
|
||||||
|
'mesh_issues': quality_issues,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register_hooks(hook_manager):
|
||||||
|
hook_manager.register_hook(
|
||||||
|
hook_point='post_mesh',
|
||||||
|
function=mesh_quality_hook,
|
||||||
|
name='mesh_quality_hook',
|
||||||
|
description='Validate mesh quality',
|
||||||
|
priority=50,
|
||||||
|
enabled=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hook Context Reference
|
||||||
|
|
||||||
|
### Standard Context Keys
|
||||||
|
|
||||||
|
| Key | Type | Available At | Description |
|
||||||
|
|-----|------|--------------|-------------|
|
||||||
|
| `trial_number` | int | All | Current trial number |
|
||||||
|
| `design_params` | dict | All | Design parameter values |
|
||||||
|
| `config` | dict | All | Optimization config |
|
||||||
|
| `working_dir` | Path | All | Study working directory |
|
||||||
|
| `model_file` | Path | pre_mesh+ | NX model file path |
|
||||||
|
| `mesh_file` | Path | post_mesh+ | Mesh file path |
|
||||||
|
| `op2_file` | Path | post_solve+ | Results file path |
|
||||||
|
| `results` | dict | post_extraction+ | Extracted results |
|
||||||
|
| `objectives` | dict | post_calculation | Computed objectives |
|
||||||
|
|
||||||
|
### Priority Guidelines
|
||||||
|
|
||||||
|
| Priority Range | Use For |
|
||||||
|
|----------------|---------|
|
||||||
|
| 1-50 | Critical hooks that must run first |
|
||||||
|
| 50-100 | Standard hooks |
|
||||||
|
| 100-150 | Logging and monitoring |
|
||||||
|
| 150+ | Cleanup and finalization |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Managing Hooks
|
||||||
|
|
||||||
|
### Enable/Disable at Runtime
|
||||||
|
|
||||||
|
```python
|
||||||
|
hook_manager.disable_hook('my_hook')
|
||||||
|
hook_manager.enable_hook('my_hook')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Hook Status
|
||||||
|
|
||||||
|
```python
|
||||||
|
hooks = hook_manager.list_hooks()
|
||||||
|
for hook in hooks:
|
||||||
|
print(f"{hook['name']}: {'enabled' if hook['enabled'] else 'disabled'}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook Execution Order
|
||||||
|
|
||||||
|
Hooks at the same point run in priority order (lower first):
|
||||||
|
```
|
||||||
|
Priority 10: trial_logger_hook
|
||||||
|
Priority 50: mesh_quality_hook
|
||||||
|
Priority 100: safety_factor_hook
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| Hook not running | Not registered | Check `register_hooks` function |
|
||||||
|
| Wrong hook point | Misnamed directory | Check directory name matches hook point |
|
||||||
|
| Context missing key | Wrong hook point | Use appropriate hook point for data needed |
|
||||||
|
| Hook error crashes trial | Unhandled exception | Add try/except in hook |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Related**: [EXT_01_CREATE_EXTRACTOR](./EXT_01_CREATE_EXTRACTOR.md)
|
||||||
|
- **System**: `optimization_engine/plugins/hook_manager.py`
|
||||||
|
- **Template**: `templates/hook_template.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial release |
|
||||||
263
docs/protocols/extensions/EXT_03_CREATE_PROTOCOL.md
Normal file
263
docs/protocols/extensions/EXT_03_CREATE_PROTOCOL.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
# EXT_03: Create New Protocol
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Create New Protocol Document
|
||||||
|
LAYER: Extensions
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: admin
|
||||||
|
LOAD_WITH: []
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This protocol guides you through creating new protocol documents for the Atomizer Protocol Operating System (POS). Use this when adding significant new system capabilities.
|
||||||
|
|
||||||
|
**Privilege Required**: admin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| Adding major new system capability | Follow this protocol |
|
||||||
|
| "create protocol", "new protocol" | Follow this protocol |
|
||||||
|
| Need to document architectural pattern | Follow this protocol |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Types
|
||||||
|
|
||||||
|
| Layer | Prefix | Purpose | Example |
|
||||||
|
|-------|--------|---------|---------|
|
||||||
|
| Operations | OP_ | How-to guides | OP_01_CREATE_STUDY |
|
||||||
|
| System | SYS_ | Core specifications | SYS_10_IMSO |
|
||||||
|
| Extensions | EXT_ | Extensibility guides | EXT_01_CREATE_EXTRACTOR |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step Guide
|
||||||
|
|
||||||
|
### Step 1: Determine Protocol Type
|
||||||
|
|
||||||
|
- **Operations (OP_)**: User-facing procedures
|
||||||
|
- **System (SYS_)**: Technical specifications
|
||||||
|
- **Extensions (EXT_)**: Developer guides
|
||||||
|
|
||||||
|
### Step 2: Assign Protocol Number
|
||||||
|
|
||||||
|
**Operations**: Sequential (OP_01, OP_02, ...)
|
||||||
|
**System**: By feature area (SYS_10=optimization, SYS_11=multi-obj, etc.)
|
||||||
|
**Extensions**: Sequential (EXT_01, EXT_02, ...)
|
||||||
|
|
||||||
|
Check existing protocols to avoid conflicts.
|
||||||
|
|
||||||
|
### Step 3: Create Protocol File
|
||||||
|
|
||||||
|
Use the template from `templates/protocol_template.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# {LAYER}_{NUMBER}_{NAME}.md
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: {Full Name}
|
||||||
|
LAYER: {Operations|System|Extensions}
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: {YYYY-MM-DD}
|
||||||
|
PRIVILEGE: {user|power_user|admin}
|
||||||
|
LOAD_WITH: [{dependencies}]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
{1-3 sentence description of what this protocol does}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| {keyword or condition} | Follow this protocol |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
{Tables with key parameters, commands, or mappings}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Specification
|
||||||
|
|
||||||
|
### Section 1: {Topic}
|
||||||
|
|
||||||
|
{Content}
|
||||||
|
|
||||||
|
### Section 2: {Topic}
|
||||||
|
|
||||||
|
{Content}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: {Scenario}
|
||||||
|
|
||||||
|
{Complete working example}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| {error} | {why} | {fix} |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Depends On**: [{protocol}]({path})
|
||||||
|
- **Used By**: [{protocol}]({path})
|
||||||
|
- **See Also**: [{related}]({path})
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | {DATE} | Initial release |
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Write Content
|
||||||
|
|
||||||
|
**Required Sections**:
|
||||||
|
1. Overview - What does this protocol do?
|
||||||
|
2. When to Use - Trigger conditions
|
||||||
|
3. Quick Reference - Fast lookup
|
||||||
|
4. Detailed Specification - Full content
|
||||||
|
5. Examples - Working examples
|
||||||
|
6. Troubleshooting - Common issues
|
||||||
|
7. Cross-References - Related protocols
|
||||||
|
8. Version History - Changes over time
|
||||||
|
|
||||||
|
**Writing Guidelines**:
|
||||||
|
- Front-load important information
|
||||||
|
- Use tables for structured data
|
||||||
|
- Include complete code examples
|
||||||
|
- Provide troubleshooting for common issues
|
||||||
|
|
||||||
|
### Step 5: Update Navigation
|
||||||
|
|
||||||
|
**docs/protocols/README.md**:
|
||||||
|
```markdown
|
||||||
|
| {NUM} | {Name} | [{Layer}]({layer}/{filename}) |
|
||||||
|
```
|
||||||
|
|
||||||
|
**.claude/skills/01_CHEATSHEET.md**:
|
||||||
|
```markdown
|
||||||
|
| {task} | {LAYER}_{NUM} | {key info} |
|
||||||
|
```
|
||||||
|
|
||||||
|
**.claude/skills/02_CONTEXT_LOADER.md**:
|
||||||
|
Add loading rules if needed.
|
||||||
|
|
||||||
|
### Step 6: Update Cross-References
|
||||||
|
|
||||||
|
Add references in related protocols:
|
||||||
|
- "Depends On" in new protocol
|
||||||
|
- "Used By" or "See Also" in existing protocols
|
||||||
|
|
||||||
|
### Step 7: Validate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check markdown syntax
|
||||||
|
# Verify all links work
|
||||||
|
# Test code examples
|
||||||
|
# Ensure consistent formatting
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Metadata
|
||||||
|
|
||||||
|
### Header Comment Block
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Full Protocol Name
|
||||||
|
LAYER: Operations|System|Extensions
|
||||||
|
VERSION: Major.Minor
|
||||||
|
STATUS: Active|Draft|Deprecated
|
||||||
|
LAST_UPDATED: YYYY-MM-DD
|
||||||
|
PRIVILEGE: user|power_user|admin
|
||||||
|
LOAD_WITH: [SYS_10, SYS_11]
|
||||||
|
-->
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status Values
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
|--------|---------|
|
||||||
|
| Draft | In development, not ready for use |
|
||||||
|
| Active | Production ready |
|
||||||
|
| Deprecated | Being phased out |
|
||||||
|
|
||||||
|
### Privilege Levels
|
||||||
|
|
||||||
|
| Level | Who Can Use |
|
||||||
|
|-------|-------------|
|
||||||
|
| user | All users |
|
||||||
|
| power_user | Developers who can extend |
|
||||||
|
| admin | Full system access |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
### Semantic Versioning
|
||||||
|
|
||||||
|
- **Major (X.0)**: Breaking changes
|
||||||
|
- **Minor (1.X)**: New features, backward compatible
|
||||||
|
- **Patch (1.0.X)**: Bug fixes (usually omit for docs)
|
||||||
|
|
||||||
|
### Version History Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 2.0 | 2025-12-15 | Redesigned architecture |
|
||||||
|
| 1.1 | 2025-12-05 | Added neural support |
|
||||||
|
| 1.0 | 2025-11-20 | Initial release |
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| Protocol not found | Wrong path | Check location and README |
|
||||||
|
| LLM not loading | Missing from context loader | Update 02_CONTEXT_LOADER.md |
|
||||||
|
| Broken links | Path changed | Update cross-references |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Template**: `templates/protocol_template.md`
|
||||||
|
- **Navigation**: `docs/protocols/README.md`
|
||||||
|
- **Context Loading**: `.claude/skills/02_CONTEXT_LOADER.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial release |
|
||||||
331
docs/protocols/extensions/EXT_04_CREATE_SKILL.md
Normal file
331
docs/protocols/extensions/EXT_04_CREATE_SKILL.md
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
# EXT_04: Create New Skill
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Create New Skill or Module
|
||||||
|
LAYER: Extensions
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: admin
|
||||||
|
LOAD_WITH: []
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This protocol guides you through creating new skills or skill modules for the LLM instruction system. Skills provide task-specific guidance to Claude sessions.
|
||||||
|
|
||||||
|
**Privilege Required**: admin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| Need new LLM capability | Follow this protocol |
|
||||||
|
| "create skill", "new skill" | Follow this protocol |
|
||||||
|
| Task pattern needs documentation | Follow this protocol |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Skill Types
|
||||||
|
|
||||||
|
| Type | Location | Purpose | Example |
|
||||||
|
|------|----------|---------|---------|
|
||||||
|
| Bootstrap | `.claude/skills/0X_*.md` | LLM orientation | 00_BOOTSTRAP.md |
|
||||||
|
| Core | `.claude/skills/core/` | Always-load skills | study-creation-core.md |
|
||||||
|
| Module | `.claude/skills/modules/` | Optional, load-on-demand | extractors-catalog.md |
|
||||||
|
| Dev | `.claude/skills/DEV_*.md` | Developer workflows | DEV_DOCUMENTATION.md |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step Guide
|
||||||
|
|
||||||
|
### Step 1: Determine Skill Type
|
||||||
|
|
||||||
|
**Bootstrap (0X_)**: System-level LLM guidance
|
||||||
|
- Task classification
|
||||||
|
- Context loading rules
|
||||||
|
- Execution patterns
|
||||||
|
|
||||||
|
**Core**: Essential task skills that are always loaded
|
||||||
|
- Study creation
|
||||||
|
- Run optimization (basic)
|
||||||
|
|
||||||
|
**Module**: Specialized skills loaded on demand
|
||||||
|
- Specific extractors
|
||||||
|
- Domain-specific (Zernike, neural)
|
||||||
|
- Advanced features
|
||||||
|
|
||||||
|
**Dev (DEV_)**: Developer-facing workflows
|
||||||
|
- Documentation maintenance
|
||||||
|
- Testing procedures
|
||||||
|
- Contribution guides
|
||||||
|
|
||||||
|
### Step 2: Create Skill File
|
||||||
|
|
||||||
|
#### For Core/Module Skills
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# {Skill Name}
|
||||||
|
|
||||||
|
**Version**: 1.0
|
||||||
|
**Purpose**: {One-line description}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
{What this skill enables Claude to do}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Load
|
||||||
|
|
||||||
|
This skill should be loaded when:
|
||||||
|
- {Condition 1}
|
||||||
|
- {Condition 2}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
{Tables with key patterns, commands}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Instructions
|
||||||
|
|
||||||
|
### Pattern 1: {Name}
|
||||||
|
|
||||||
|
{Step-by-step instructions}
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
\`\`\`python
|
||||||
|
{code example}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Pattern 2: {Name}
|
||||||
|
|
||||||
|
{Step-by-step instructions}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Templates
|
||||||
|
|
||||||
|
### Template 1: {Name}
|
||||||
|
|
||||||
|
\`\`\`python
|
||||||
|
{copy-paste ready code}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Before completing:
|
||||||
|
- [ ] {Check 1}
|
||||||
|
- [ ] {Check 2}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- **Protocol**: [{related}]({path})
|
||||||
|
- **Module**: [{related}]({path})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Register Skill
|
||||||
|
|
||||||
|
#### For Bootstrap Skills
|
||||||
|
|
||||||
|
Add to `00_BOOTSTRAP.md` task classification tree.
|
||||||
|
|
||||||
|
#### For Core Skills
|
||||||
|
|
||||||
|
Add to `02_CONTEXT_LOADER.md`:
|
||||||
|
```yaml
|
||||||
|
{TASK_TYPE}:
|
||||||
|
always_load:
|
||||||
|
- core/{skill_name}.md
|
||||||
|
```
|
||||||
|
|
||||||
|
#### For Modules
|
||||||
|
|
||||||
|
Add to `02_CONTEXT_LOADER.md`:
|
||||||
|
```yaml
|
||||||
|
{TASK_TYPE}:
|
||||||
|
load_if:
|
||||||
|
- modules/{skill_name}.md: "{condition}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Update Navigation
|
||||||
|
|
||||||
|
Add to `01_CHEATSHEET.md` if relevant to common tasks.
|
||||||
|
|
||||||
|
### Step 5: Test
|
||||||
|
|
||||||
|
Test with fresh Claude session:
|
||||||
|
1. Start new conversation
|
||||||
|
2. Describe task that should trigger skill
|
||||||
|
3. Verify correct skill is loaded
|
||||||
|
4. Verify skill instructions are followed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Skill Design Guidelines
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
- **Front-load**: Most important info first
|
||||||
|
- **Tables**: Use for structured data
|
||||||
|
- **Code blocks**: Complete, copy-paste ready
|
||||||
|
- **Checklists**: For validation steps
|
||||||
|
|
||||||
|
### Content
|
||||||
|
|
||||||
|
- **Task-focused**: What should Claude DO?
|
||||||
|
- **Prescriptive**: Clear instructions, not options
|
||||||
|
- **Examples**: Show expected patterns
|
||||||
|
- **Validation**: How to verify success
|
||||||
|
|
||||||
|
### Length Guidelines
|
||||||
|
|
||||||
|
| Skill Type | Target Lines | Rationale |
|
||||||
|
|------------|--------------|-----------|
|
||||||
|
| Bootstrap | 100-200 | Quick orientation |
|
||||||
|
| Core | 500-1000 | Comprehensive task guide |
|
||||||
|
| Module | 150-400 | Focused specialization |
|
||||||
|
|
||||||
|
### Avoid
|
||||||
|
|
||||||
|
- Duplicating protocol content (reference instead)
|
||||||
|
- Vague instructions ("consider" → "do")
|
||||||
|
- Missing examples
|
||||||
|
- Untested code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module vs Protocol
|
||||||
|
|
||||||
|
**Skills** teach Claude HOW to interact:
|
||||||
|
- Conversation patterns
|
||||||
|
- Code templates
|
||||||
|
- Validation steps
|
||||||
|
- User interaction
|
||||||
|
|
||||||
|
**Protocols** document WHAT exists:
|
||||||
|
- Technical specifications
|
||||||
|
- Configuration options
|
||||||
|
- Architecture details
|
||||||
|
- Troubleshooting
|
||||||
|
|
||||||
|
Skills REFERENCE protocols, don't duplicate them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example: Domain-Specific Module
|
||||||
|
|
||||||
|
`modules/thermal-optimization.md`:
|
||||||
|
```markdown
|
||||||
|
# Thermal Optimization Module
|
||||||
|
|
||||||
|
**Version**: 1.0
|
||||||
|
**Purpose**: Specialized guidance for thermal FEA optimization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Load
|
||||||
|
|
||||||
|
Load when:
|
||||||
|
- "thermal", "temperature", "heat" in user request
|
||||||
|
- Optimizing for thermal properties
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Physics | Extractor | Unit |
|
||||||
|
|---------|-----------|------|
|
||||||
|
| Max temp | E11 | K |
|
||||||
|
| Gradient | E12 | K/mm |
|
||||||
|
| Heat flux | E13 | W/m² |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objective Patterns
|
||||||
|
|
||||||
|
### Minimize Max Temperature
|
||||||
|
|
||||||
|
\`\`\`python
|
||||||
|
from optimization_engine.extractors import extract_temperature
|
||||||
|
|
||||||
|
def objective(trial):
|
||||||
|
# ... run simulation ...
|
||||||
|
temp_result = extract_temperature(op2_file)
|
||||||
|
return temp_result['max_temperature']
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Minimize Thermal Gradient
|
||||||
|
|
||||||
|
\`\`\`python
|
||||||
|
from optimization_engine.extractors import extract_thermal_gradient
|
||||||
|
|
||||||
|
def objective(trial):
|
||||||
|
# ... run simulation ...
|
||||||
|
grad_result = extract_thermal_gradient(op2_file)
|
||||||
|
return grad_result['max_gradient']
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Example
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"objectives": [
|
||||||
|
{
|
||||||
|
"name": "max_temperature",
|
||||||
|
"type": "minimize",
|
||||||
|
"unit": "K",
|
||||||
|
"description": "Maximum temperature in component"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- **Extractors**: E11, E12, E13 in SYS_12
|
||||||
|
- **Protocol**: See OP_01 for study creation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| Skill not loaded | Not in context loader | Add loading rule |
|
||||||
|
| Wrong skill loaded | Ambiguous triggers | Refine conditions |
|
||||||
|
| Instructions not followed | Too vague | Make prescriptive |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Context Loader**: `.claude/skills/02_CONTEXT_LOADER.md`
|
||||||
|
- **Bootstrap**: `.claude/skills/00_BOOTSTRAP.md`
|
||||||
|
- **Related**: [EXT_03_CREATE_PROTOCOL](./EXT_03_CREATE_PROTOCOL.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial release |
|
||||||
186
docs/protocols/extensions/templates/extractor_template.py
Normal file
186
docs/protocols/extensions/templates/extractor_template.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
"""
|
||||||
|
Extract {Physics Name} from FEA results.
|
||||||
|
|
||||||
|
This is a template for creating new physics extractors.
|
||||||
|
Copy this file to optimization_engine/extractors/extract_{physics}.py
|
||||||
|
and customize for your specific physics extraction.
|
||||||
|
|
||||||
|
Author: {Your Name}
|
||||||
|
Created: {Date}
|
||||||
|
Version: 1.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional, Union
|
||||||
|
from pyNastran.op2.op2 import OP2
|
||||||
|
|
||||||
|
|
||||||
|
def extract_{physics}(
|
||||||
|
op2_file: Union[str, Path],
|
||||||
|
subcase: int = 1,
|
||||||
|
# Add other parameters specific to your physics
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract {physics description} from OP2 file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
op2_file: Path to the OP2 results file
|
||||||
|
subcase: Subcase number to extract (default: 1)
|
||||||
|
# Document other parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- '{main_result}': The primary result value ({unit})
|
||||||
|
- '{secondary_result}': Secondary result info
|
||||||
|
- 'subcase': The subcase extracted
|
||||||
|
- 'unit': Unit of the result
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If OP2 file doesn't exist
|
||||||
|
KeyError: If subcase not found in results
|
||||||
|
ValueError: If result data is invalid
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = extract_{physics}('model.op2', subcase=1)
|
||||||
|
>>> print(result['{main_result}'])
|
||||||
|
123.45
|
||||||
|
>>> print(result['unit'])
|
||||||
|
'{unit}'
|
||||||
|
"""
|
||||||
|
# Convert to Path for consistency
|
||||||
|
op2_file = Path(op2_file)
|
||||||
|
|
||||||
|
# Validate file exists
|
||||||
|
if not op2_file.exists():
|
||||||
|
raise FileNotFoundError(f"OP2 file not found: {op2_file}")
|
||||||
|
|
||||||
|
# Read OP2 file
|
||||||
|
op2 = OP2()
|
||||||
|
op2.read_op2(str(op2_file))
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# CUSTOMIZE: Your extraction logic here
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
# Example: Access displacement data
|
||||||
|
# if subcase not in op2.displacements:
|
||||||
|
# raise KeyError(f"Subcase {subcase} not found in displacement results")
|
||||||
|
# data = op2.displacements[subcase]
|
||||||
|
|
||||||
|
# Example: Access stress data
|
||||||
|
# if subcase not in op2.cquad4_stress:
|
||||||
|
# raise KeyError(f"Subcase {subcase} not found in stress results")
|
||||||
|
# stress_data = op2.cquad4_stress[subcase]
|
||||||
|
|
||||||
|
# Example: Process data
|
||||||
|
# values = data.data # numpy array
|
||||||
|
# max_value = values.max()
|
||||||
|
# max_index = values.argmax()
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Replace with your actual computation
|
||||||
|
# =========================================
|
||||||
|
main_result = 0.0 # TODO: Compute actual value
|
||||||
|
secondary_result = 0 # TODO: Compute actual value
|
||||||
|
|
||||||
|
return {
|
||||||
|
'{main_result}': main_result,
|
||||||
|
'{secondary_result}': secondary_result,
|
||||||
|
'subcase': subcase,
|
||||||
|
'unit': '{unit}',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Optional: Class-based extractor for complex cases
|
||||||
|
class {Physics}Extractor:
|
||||||
|
"""
|
||||||
|
Class-based extractor for {physics} with state management.
|
||||||
|
|
||||||
|
Use this pattern when:
|
||||||
|
- Extraction requires multiple steps
|
||||||
|
- You need to cache the OP2 data
|
||||||
|
- Configuration is complex
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> extractor = {Physics}Extractor('model.op2', config={'option': value})
|
||||||
|
>>> result = extractor.extract(subcase=1)
|
||||||
|
>>> print(result)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
op2_file: Union[str, Path],
|
||||||
|
bdf_file: Optional[Union[str, Path]] = None,
|
||||||
|
**config
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the extractor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
op2_file: Path to OP2 results file
|
||||||
|
bdf_file: Optional path to BDF mesh file (for node coordinates)
|
||||||
|
**config: Additional configuration options
|
||||||
|
"""
|
||||||
|
self.op2_file = Path(op2_file)
|
||||||
|
self.bdf_file = Path(bdf_file) if bdf_file else None
|
||||||
|
self.config = config
|
||||||
|
self._op2 = None # Lazy-loaded
|
||||||
|
|
||||||
|
def _load_op2(self) -> OP2:
|
||||||
|
"""Lazy load OP2 file (caches result)."""
|
||||||
|
if self._op2 is None:
|
||||||
|
self._op2 = OP2()
|
||||||
|
self._op2.read_op2(str(self.op2_file))
|
||||||
|
return self._op2
|
||||||
|
|
||||||
|
def extract(self, subcase: int = 1) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract results for given subcase.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subcase: Subcase number
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with extraction results
|
||||||
|
"""
|
||||||
|
op2 = self._load_op2()
|
||||||
|
|
||||||
|
# TODO: Implement your extraction logic
|
||||||
|
# Use self.config for configuration options
|
||||||
|
|
||||||
|
return {
|
||||||
|
'{main_result}': 0.0,
|
||||||
|
'subcase': subcase,
|
||||||
|
}
|
||||||
|
|
||||||
|
def extract_all_subcases(self) -> Dict[int, Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Extract results for all available subcases.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping subcase number to results
|
||||||
|
"""
|
||||||
|
op2 = self._load_op2()
|
||||||
|
|
||||||
|
# TODO: Find available subcases
|
||||||
|
# available_subcases = list(op2.displacements.keys())
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
# for sc in available_subcases:
|
||||||
|
# results[sc] = self.extract(subcase=sc)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# After creating your extractor:
|
||||||
|
# 1. Add to optimization_engine/extractors/__init__.py:
|
||||||
|
# from .extract_{physics} import extract_{physics}
|
||||||
|
# __all__ = [..., 'extract_{physics}']
|
||||||
|
#
|
||||||
|
# 2. Update docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md
|
||||||
|
# - Add to Quick Reference table
|
||||||
|
# - Add detailed section with example
|
||||||
|
#
|
||||||
|
# 3. Create test file: tests/test_extract_{physics}.py
|
||||||
|
# =========================================
|
||||||
213
docs/protocols/extensions/templates/hook_template.py
Normal file
213
docs/protocols/extensions/templates/hook_template.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
"""
|
||||||
|
{Hook Name} - Lifecycle Hook Plugin
|
||||||
|
|
||||||
|
This is a template for creating new lifecycle hooks.
|
||||||
|
Copy this file to optimization_engine/plugins/{hook_point}/{hook_name}.py
|
||||||
|
|
||||||
|
Available hook points:
|
||||||
|
- pre_mesh: Before meshing
|
||||||
|
- post_mesh: After meshing
|
||||||
|
- pre_solve: Before solver execution
|
||||||
|
- post_solve: After solver completion
|
||||||
|
- post_extraction: After result extraction
|
||||||
|
- post_calculation: After objective calculation
|
||||||
|
- custom_objective: Custom objective functions
|
||||||
|
|
||||||
|
Author: {Your Name}
|
||||||
|
Created: {Date}
|
||||||
|
Version: 1.0
|
||||||
|
Hook Point: {hook_point}
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def {hook_name}_hook(context: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
{Description of what this hook does}.
|
||||||
|
|
||||||
|
This hook runs at the {hook_point} stage of the optimization trial.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Dictionary containing trial context:
|
||||||
|
- trial_number (int): Current trial number
|
||||||
|
- design_params (dict): Current design parameter values
|
||||||
|
- config (dict): Optimization configuration
|
||||||
|
- working_dir (Path): Study working directory
|
||||||
|
|
||||||
|
For post_solve and later:
|
||||||
|
- op2_file (Path): Path to OP2 results file
|
||||||
|
- solve_success (bool): Whether solve succeeded
|
||||||
|
- solve_time (float): Solve duration in seconds
|
||||||
|
|
||||||
|
For post_extraction and later:
|
||||||
|
- results (dict): Extracted results so far
|
||||||
|
|
||||||
|
For post_calculation:
|
||||||
|
- objectives (dict): Computed objective values
|
||||||
|
- constraints (dict): Constraint values
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with computed values or modifications.
|
||||||
|
These values are added to the trial context.
|
||||||
|
Return empty dict {} if no modifications needed.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Any exception will be logged but won't stop the trial
|
||||||
|
unless you want it to (raise optuna.TrialPruned instead)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> context = {'trial_number': 1, 'design_params': {'x': 5.0}}
|
||||||
|
>>> result = {hook_name}_hook(context)
|
||||||
|
>>> print(result)
|
||||||
|
{{'{computed_key}': 123.45}}
|
||||||
|
"""
|
||||||
|
# =========================================
|
||||||
|
# Access context values
|
||||||
|
# =========================================
|
||||||
|
trial_num = context.get('trial_number', 0)
|
||||||
|
design_params = context.get('design_params', {})
|
||||||
|
config = context.get('config', {})
|
||||||
|
working_dir = context.get('working_dir', Path('.'))
|
||||||
|
|
||||||
|
# For post_solve hooks and later:
|
||||||
|
# op2_file = context.get('op2_file')
|
||||||
|
# solve_success = context.get('solve_success', False)
|
||||||
|
|
||||||
|
# For post_extraction hooks and later:
|
||||||
|
# results = context.get('results', {})
|
||||||
|
|
||||||
|
# For post_calculation hooks:
|
||||||
|
# objectives = context.get('objectives', {})
|
||||||
|
# constraints = context.get('constraints', {})
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Your hook logic here
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
# Example: Log trial start (pre_solve hook)
|
||||||
|
# print(f"[Hook] Trial {trial_num} starting with params: {design_params}")
|
||||||
|
|
||||||
|
# Example: Compute derived quantity (post_extraction hook)
|
||||||
|
# max_stress = results.get('max_von_mises', 0)
|
||||||
|
# yield_strength = config.get('material', {}).get('yield_strength', 250)
|
||||||
|
# safety_factor = yield_strength / max(max_stress, 1e-6)
|
||||||
|
|
||||||
|
# Example: Write log file (post_calculation hook)
|
||||||
|
# log_entry = {
|
||||||
|
# 'trial': trial_num,
|
||||||
|
# 'timestamp': datetime.now().isoformat(),
|
||||||
|
# 'objectives': context.get('objectives', {}),
|
||||||
|
# }
|
||||||
|
# with open(working_dir / 'trial_log.jsonl', 'a') as f:
|
||||||
|
# f.write(json.dumps(log_entry) + '\n')
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Return computed values
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
# Values returned here are added to the context
|
||||||
|
# and can be accessed by later hooks or the optimizer
|
||||||
|
|
||||||
|
return {
|
||||||
|
# '{computed_key}': computed_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register_hooks(hook_manager) -> None:
|
||||||
|
"""
|
||||||
|
Register this hook with the hook manager.
|
||||||
|
|
||||||
|
This function is called automatically when plugins are discovered.
|
||||||
|
It must be named exactly 'register_hooks' and take one argument.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hook_manager: The HookManager instance from optimization_engine
|
||||||
|
"""
|
||||||
|
hook_manager.register_hook(
|
||||||
|
hook_point='{hook_point}', # pre_mesh, post_mesh, pre_solve, etc.
|
||||||
|
function={hook_name}_hook,
|
||||||
|
name='{hook_name}_hook',
|
||||||
|
description='{Brief description of what this hook does}',
|
||||||
|
priority=100, # Lower number = runs earlier (1-200 typical range)
|
||||||
|
enabled=True # Set to False to disable by default
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Optional: Helper functions
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
def _helper_function(data: Any) -> Any:
|
||||||
|
"""
|
||||||
|
Private helper function for the hook.
|
||||||
|
|
||||||
|
Keep hook logic clean by extracting complex operations
|
||||||
|
into helper functions.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# After creating your hook:
|
||||||
|
#
|
||||||
|
# 1. Place in correct directory:
|
||||||
|
# optimization_engine/plugins/{hook_point}/{hook_name}.py
|
||||||
|
#
|
||||||
|
# 2. Hook is auto-discovered - no __init__.py changes needed
|
||||||
|
#
|
||||||
|
# 3. Test the hook:
|
||||||
|
# python -c "
|
||||||
|
# from optimization_engine.plugins.hook_manager import HookManager
|
||||||
|
# hm = HookManager()
|
||||||
|
# hm.discover_plugins()
|
||||||
|
# print(hm.list_hooks())
|
||||||
|
# "
|
||||||
|
#
|
||||||
|
# 4. Update documentation if significant:
|
||||||
|
# - Add to EXT_02_CREATE_HOOK.md examples section
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================
|
||||||
|
# Example hooks for reference
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
def example_logger_hook(context: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Example: Simple trial logger for pre_solve."""
|
||||||
|
trial = context.get('trial_number', 0)
|
||||||
|
params = context.get('design_params', {})
|
||||||
|
print(f"[LOG] Trial {trial} starting: {params}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def example_safety_factor_hook(context: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Example: Safety factor calculator for post_extraction."""
|
||||||
|
results = context.get('results', {})
|
||||||
|
max_stress = results.get('max_von_mises', 0)
|
||||||
|
|
||||||
|
if max_stress > 0:
|
||||||
|
safety_factor = 250.0 / max_stress # Assuming 250 MPa yield
|
||||||
|
else:
|
||||||
|
safety_factor = float('inf')
|
||||||
|
|
||||||
|
return {'safety_factor': safety_factor}
|
||||||
|
|
||||||
|
|
||||||
|
def example_validator_hook(context: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Example: Result validator for post_solve."""
|
||||||
|
import optuna
|
||||||
|
|
||||||
|
solve_success = context.get('solve_success', False)
|
||||||
|
op2_file = context.get('op2_file')
|
||||||
|
|
||||||
|
if not solve_success:
|
||||||
|
raise optuna.TrialPruned("Solve failed")
|
||||||
|
|
||||||
|
if op2_file and not Path(op2_file).exists():
|
||||||
|
raise optuna.TrialPruned("OP2 file not generated")
|
||||||
|
|
||||||
|
return {'validation_passed': True}
|
||||||
112
docs/protocols/extensions/templates/protocol_template.md
Normal file
112
docs/protocols/extensions/templates/protocol_template.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# {LAYER}_{NUMBER}_{NAME}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: {Full Protocol Name}
|
||||||
|
LAYER: {Operations|System|Extensions}
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: {YYYY-MM-DD}
|
||||||
|
PRIVILEGE: {user|power_user|admin}
|
||||||
|
LOAD_WITH: [{dependency_protocols}]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
{1-3 sentence description of what this protocol does and why it exists.}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| {keyword or user intent} | Follow this protocol |
|
||||||
|
| {condition} | Follow this protocol |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
{Key information in table format for fast lookup}
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| {param} | {value} | {description} |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Specification
|
||||||
|
|
||||||
|
### Section 1: {Topic}
|
||||||
|
|
||||||
|
{Detailed content}
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Code example if applicable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Section 2: {Topic}
|
||||||
|
|
||||||
|
{Detailed content}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
{If applicable, show configuration examples}
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"setting": "value"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: {Scenario Name}
|
||||||
|
|
||||||
|
{Complete working example with context}
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Full working code example
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: {Scenario Name}
|
||||||
|
|
||||||
|
{Another example showing different use case}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| {error message or symptom} | {root cause} | {how to fix} |
|
||||||
|
| {symptom} | {cause} | {solution} |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Depends On**: [{protocol_name}]({relative_path})
|
||||||
|
- **Used By**: [{protocol_name}]({relative_path})
|
||||||
|
- **See Also**: [{related_doc}]({path})
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Files
|
||||||
|
|
||||||
|
{If applicable, list the code files that implement this protocol}
|
||||||
|
|
||||||
|
- `path/to/file.py` - {description}
|
||||||
|
- `path/to/other.py` - {description}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | {YYYY-MM-DD} | Initial release |
|
||||||
403
docs/protocols/operations/OP_01_CREATE_STUDY.md
Normal file
403
docs/protocols/operations/OP_01_CREATE_STUDY.md
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
# OP_01: Create Optimization Study
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Create Optimization Study
|
||||||
|
LAYER: Operations
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: user
|
||||||
|
LOAD_WITH: [core/study-creation-core.md]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This protocol guides you through creating a complete Atomizer optimization study from scratch. It covers gathering requirements, generating configuration files, and validating setup.
|
||||||
|
|
||||||
|
**Skill to Load**: `.claude/skills/core/study-creation-core.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| "new study", "create study" | Follow this protocol |
|
||||||
|
| "set up optimization" | Follow this protocol |
|
||||||
|
| "optimize my design" | Follow this protocol |
|
||||||
|
| User provides NX model | Assess and follow this protocol |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Required Outputs**:
|
||||||
|
| File | Purpose | Location |
|
||||||
|
|------|---------|----------|
|
||||||
|
| `optimization_config.json` | Design vars, objectives, constraints | `1_setup/` |
|
||||||
|
| `run_optimization.py` | Execution script | Study root |
|
||||||
|
| `README.md` | Engineering documentation | Study root |
|
||||||
|
| `STUDY_REPORT.md` | Results template | Study root |
|
||||||
|
|
||||||
|
**Study Structure**:
|
||||||
|
```
|
||||||
|
studies/{study_name}/
|
||||||
|
├── 1_setup/
|
||||||
|
│ ├── model/ # NX files (.prt, .sim, .fem)
|
||||||
|
│ └── optimization_config.json
|
||||||
|
├── 2_results/ # Created during run
|
||||||
|
├── README.md # MANDATORY
|
||||||
|
├── STUDY_REPORT.md # MANDATORY
|
||||||
|
└── run_optimization.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Steps
|
||||||
|
|
||||||
|
### Step 1: Gather Requirements
|
||||||
|
|
||||||
|
**Ask the user**:
|
||||||
|
1. What are you trying to optimize? (objective)
|
||||||
|
2. What can you change? (design variables)
|
||||||
|
3. What limits must be respected? (constraints)
|
||||||
|
4. Where are your NX files?
|
||||||
|
|
||||||
|
**Example Dialog**:
|
||||||
|
```
|
||||||
|
User: "I want to optimize my bracket"
|
||||||
|
You: "What should I optimize for - minimum mass, maximum stiffness,
|
||||||
|
target frequency, or something else?"
|
||||||
|
User: "Minimize mass while keeping stress below 250 MPa"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Analyze Model (Introspection)
|
||||||
|
|
||||||
|
**MANDATORY**: When user provides NX files, run comprehensive introspection:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.hooks.nx_cad.model_introspection import (
|
||||||
|
introspect_part,
|
||||||
|
introspect_simulation,
|
||||||
|
introspect_op2,
|
||||||
|
introspect_study
|
||||||
|
)
|
||||||
|
|
||||||
|
# Introspect the part file to get expressions, mass, features
|
||||||
|
part_info = introspect_part("C:/path/to/model.prt")
|
||||||
|
|
||||||
|
# Introspect the simulation to get solutions, BCs, loads
|
||||||
|
sim_info = introspect_simulation("C:/path/to/model.sim")
|
||||||
|
|
||||||
|
# If OP2 exists, check what results are available
|
||||||
|
op2_info = introspect_op2("C:/path/to/results.op2")
|
||||||
|
|
||||||
|
# Or introspect entire study directory at once
|
||||||
|
study_info = introspect_study("studies/my_study/")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Introspection Report Contents**:
|
||||||
|
|
||||||
|
| Source | Information Extracted |
|
||||||
|
|--------|----------------------|
|
||||||
|
| `.prt` | Expressions (count, values, types), bodies, mass, material, features |
|
||||||
|
| `.sim` | Solutions, boundary conditions, loads, materials, mesh info, output requests |
|
||||||
|
| `.op2` | Available results (displacement, stress, strain, SPC forces, etc.), subcases |
|
||||||
|
|
||||||
|
**Generate Introspection Report** at study creation:
|
||||||
|
1. Save report to `studies/{study_name}/MODEL_INTROSPECTION.md`
|
||||||
|
2. Include summary of what's available for optimization
|
||||||
|
3. List potential design variables (expressions)
|
||||||
|
4. List extractable results (from OP2)
|
||||||
|
|
||||||
|
**Key Questions Answered by Introspection**:
|
||||||
|
- What expressions exist? (potential design variables)
|
||||||
|
- What solution types? (static, modal, etc.)
|
||||||
|
- What results are available in OP2? (displacement, stress, SPC forces)
|
||||||
|
- Multi-solution required? (static + modal = set `solution_name=None`)
|
||||||
|
|
||||||
|
### Step 3: Select Protocol
|
||||||
|
|
||||||
|
Based on objectives:
|
||||||
|
|
||||||
|
| Scenario | Protocol | Sampler |
|
||||||
|
|----------|----------|---------|
|
||||||
|
| Single objective | Protocol 10 (IMSO) | TPE, CMA-ES, or GP |
|
||||||
|
| 2-3 objectives | Protocol 11 | NSGA-II |
|
||||||
|
| >50 trials, need speed | Protocol 14 | + Neural acceleration |
|
||||||
|
|
||||||
|
See [SYS_10_IMSO](../system/SYS_10_IMSO.md), [SYS_11_MULTI_OBJECTIVE](../system/SYS_11_MULTI_OBJECTIVE.md).
|
||||||
|
|
||||||
|
### Step 4: Select Extractors
|
||||||
|
|
||||||
|
Match physics to extractors from [SYS_12_EXTRACTOR_LIBRARY](../system/SYS_12_EXTRACTOR_LIBRARY.md):
|
||||||
|
|
||||||
|
| Need | Extractor ID | Function |
|
||||||
|
|------|--------------|----------|
|
||||||
|
| Max displacement | E1 | `extract_displacement()` |
|
||||||
|
| Natural frequency | E2 | `extract_frequency()` |
|
||||||
|
| Von Mises stress | E3 | `extract_solid_stress()` |
|
||||||
|
| Mass from BDF | E4 | `extract_mass_from_bdf()` |
|
||||||
|
| Mass from NX | E5 | `extract_mass_from_expression()` |
|
||||||
|
| Wavefront error | E8-E10 | Zernike extractors |
|
||||||
|
|
||||||
|
### Step 5: Generate Configuration
|
||||||
|
|
||||||
|
Create `optimization_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"study_name": "bracket_optimization",
|
||||||
|
"description": "Minimize bracket mass while meeting stress constraint",
|
||||||
|
|
||||||
|
"design_variables": [
|
||||||
|
{
|
||||||
|
"name": "thickness",
|
||||||
|
"type": "continuous",
|
||||||
|
"min": 2.0,
|
||||||
|
"max": 10.0,
|
||||||
|
"unit": "mm",
|
||||||
|
"description": "Wall thickness"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"objectives": [
|
||||||
|
{
|
||||||
|
"name": "mass",
|
||||||
|
"type": "minimize",
|
||||||
|
"unit": "kg",
|
||||||
|
"description": "Total bracket mass"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"constraints": [
|
||||||
|
{
|
||||||
|
"name": "max_stress",
|
||||||
|
"type": "less_than",
|
||||||
|
"value": 250.0,
|
||||||
|
"unit": "MPa",
|
||||||
|
"description": "Maximum allowable von Mises stress"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"simulation": {
|
||||||
|
"model_file": "1_setup/model/bracket.prt",
|
||||||
|
"sim_file": "1_setup/model/bracket.sim",
|
||||||
|
"solver": "nastran",
|
||||||
|
"solution_name": null
|
||||||
|
},
|
||||||
|
|
||||||
|
"optimization_settings": {
|
||||||
|
"protocol": "protocol_10_single_objective",
|
||||||
|
"sampler": "TPESampler",
|
||||||
|
"n_trials": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Generate run_optimization.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
{study_name} - Optimization Runner
|
||||||
|
Generated by Atomizer LLM
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add optimization engine to path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
|
from optimization_engine.nx_solver import NXSolver
|
||||||
|
from optimization_engine.extractors import extract_displacement, extract_solid_stress
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
STUDY_DIR = Path(__file__).parent
|
||||||
|
MODEL_DIR = STUDY_DIR / "1_setup" / "model"
|
||||||
|
RESULTS_DIR = STUDY_DIR / "2_results"
|
||||||
|
|
||||||
|
def objective(trial):
|
||||||
|
"""Optimization objective function."""
|
||||||
|
# Sample design variables
|
||||||
|
thickness = trial.suggest_float("thickness", 2.0, 10.0)
|
||||||
|
|
||||||
|
# Update NX model and solve
|
||||||
|
nx_solver = NXSolver(...)
|
||||||
|
result = nx_solver.run_simulation(
|
||||||
|
sim_file=MODEL_DIR / "bracket.sim",
|
||||||
|
working_dir=MODEL_DIR,
|
||||||
|
expression_updates={"thickness": thickness}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result['success']:
|
||||||
|
raise optuna.TrialPruned("Simulation failed")
|
||||||
|
|
||||||
|
# Extract results using library extractors
|
||||||
|
op2_file = result['op2_file']
|
||||||
|
stress_result = extract_solid_stress(op2_file)
|
||||||
|
max_stress = stress_result['max_von_mises']
|
||||||
|
|
||||||
|
# Check constraint
|
||||||
|
if max_stress > 250.0:
|
||||||
|
raise optuna.TrialPruned(f"Stress constraint violated: {max_stress} MPa")
|
||||||
|
|
||||||
|
# Return objective
|
||||||
|
mass = extract_mass(...)
|
||||||
|
return mass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run optimization
|
||||||
|
import optuna
|
||||||
|
study = optuna.create_study(direction="minimize")
|
||||||
|
study.optimize(objective, n_trials=50)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Generate Documentation
|
||||||
|
|
||||||
|
**README.md** (11 sections required):
|
||||||
|
1. Engineering Problem
|
||||||
|
2. Mathematical Formulation
|
||||||
|
3. Optimization Algorithm
|
||||||
|
4. Simulation Pipeline
|
||||||
|
5. Result Extraction Methods
|
||||||
|
6. Neural Acceleration (if applicable)
|
||||||
|
7. Study File Structure
|
||||||
|
8. Results Location
|
||||||
|
9. Quick Start
|
||||||
|
10. Configuration Reference
|
||||||
|
11. References
|
||||||
|
|
||||||
|
**STUDY_REPORT.md** (template):
|
||||||
|
```markdown
|
||||||
|
# Study Report: {study_name}
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
- Trials completed: _pending_
|
||||||
|
- Best objective: _pending_
|
||||||
|
- Constraint satisfaction: _pending_
|
||||||
|
|
||||||
|
## Optimization Progress
|
||||||
|
_To be filled after run_
|
||||||
|
|
||||||
|
## Best Designs Found
|
||||||
|
_To be filled after run_
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
_To be filled after analysis_
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Validate NX Model File Chain
|
||||||
|
|
||||||
|
**CRITICAL**: NX simulation files have parent-child dependencies. ALL linked files must be copied to the study folder.
|
||||||
|
|
||||||
|
**Required File Chain Check**:
|
||||||
|
```
|
||||||
|
.sim (Simulation)
|
||||||
|
└── .fem (FEM)
|
||||||
|
└── _i.prt (Idealized Part) ← OFTEN MISSING!
|
||||||
|
└── .prt (Geometry Part)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Steps**:
|
||||||
|
1. Open the `.sim` file in NX
|
||||||
|
2. Go to **Assemblies → Assembly Navigator** or check **Part Navigator**
|
||||||
|
3. Identify ALL child components (especially `*_i.prt` idealized parts)
|
||||||
|
4. Copy ALL linked files to `1_setup/model/`
|
||||||
|
|
||||||
|
**Common Issue**: The `_i.prt` (idealized part) is often forgotten. Without it:
|
||||||
|
- `UpdateFemodel()` runs but mesh doesn't change
|
||||||
|
- Geometry changes don't propagate to FEM
|
||||||
|
- All optimization trials produce identical results
|
||||||
|
|
||||||
|
**File Checklist**:
|
||||||
|
| File Pattern | Description | Required |
|
||||||
|
|--------------|-------------|----------|
|
||||||
|
| `*.prt` | Geometry part | ✅ Always |
|
||||||
|
| `*_i.prt` | Idealized part | ✅ If FEM uses idealization |
|
||||||
|
| `*.fem` | FEM file | ✅ Always |
|
||||||
|
| `*.sim` | Simulation file | ✅ Always |
|
||||||
|
|
||||||
|
**Introspection should report**:
|
||||||
|
- List of all parts referenced by .sim
|
||||||
|
- Warning if any referenced parts are missing from study folder
|
||||||
|
|
||||||
|
### Step 9: Final Validation Checklist
|
||||||
|
|
||||||
|
Before running:
|
||||||
|
|
||||||
|
- [ ] NX files exist in `1_setup/model/`
|
||||||
|
- [ ] **ALL child parts copied** (especially `*_i.prt`)
|
||||||
|
- [ ] Expression names match model
|
||||||
|
- [ ] Config validates (JSON schema)
|
||||||
|
- [ ] `run_optimization.py` has no syntax errors
|
||||||
|
- [ ] README.md has all 11 sections
|
||||||
|
- [ ] STUDY_REPORT.md template exists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Simple Bracket
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Optimize my bracket.prt for minimum mass, stress < 250 MPa"
|
||||||
|
|
||||||
|
Generated config:
|
||||||
|
- 1 design variable (thickness)
|
||||||
|
- 1 objective (minimize mass)
|
||||||
|
- 1 constraint (stress < 250)
|
||||||
|
- Protocol 10, TPE sampler
|
||||||
|
- 50 trials
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Multi-Objective Beam
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Minimize mass AND maximize stiffness for my beam"
|
||||||
|
|
||||||
|
Generated config:
|
||||||
|
- 2 design variables (width, height)
|
||||||
|
- 2 objectives (minimize mass, maximize stiffness)
|
||||||
|
- Protocol 11, NSGA-II sampler
|
||||||
|
- 50 trials (Pareto front)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Telescope Mirror
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Minimize wavefront error at 40deg vs 20deg reference"
|
||||||
|
|
||||||
|
Generated config:
|
||||||
|
- Multiple design variables (mount positions)
|
||||||
|
- 1 objective (minimize relative WFE)
|
||||||
|
- Zernike extractor E9
|
||||||
|
- Protocol 10
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| "Expression not found" | Name mismatch | Verify expression names in NX |
|
||||||
|
| "No feasible designs" | Constraints too tight | Relax constraint values |
|
||||||
|
| Config validation fails | Missing required field | Check JSON schema |
|
||||||
|
| Import error | Wrong path | Check sys.path setup |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Depends On**: [SYS_12_EXTRACTOR_LIBRARY](../system/SYS_12_EXTRACTOR_LIBRARY.md)
|
||||||
|
- **Next Step**: [OP_02_RUN_OPTIMIZATION](./OP_02_RUN_OPTIMIZATION.md)
|
||||||
|
- **Skill**: `.claude/skills/core/study-creation-core.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial release |
|
||||||
297
docs/protocols/operations/OP_02_RUN_OPTIMIZATION.md
Normal file
297
docs/protocols/operations/OP_02_RUN_OPTIMIZATION.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# OP_02: Run Optimization
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Run Optimization
|
||||||
|
LAYER: Operations
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: user
|
||||||
|
LOAD_WITH: []
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This protocol covers executing optimization runs, including pre-flight validation, execution modes, monitoring, and handling common issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| "start", "run", "execute" | Follow this protocol |
|
||||||
|
| "begin optimization" | Follow this protocol |
|
||||||
|
| Study setup complete | Execute this protocol |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Start Command**:
|
||||||
|
```bash
|
||||||
|
conda activate atomizer
|
||||||
|
cd studies/{study_name}
|
||||||
|
python run_optimization.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Options**:
|
||||||
|
| Flag | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `--n-trials 100` | Override trial count |
|
||||||
|
| `--resume` | Continue interrupted run |
|
||||||
|
| `--test` | Run single trial for validation |
|
||||||
|
| `--export-training` | Export data for neural training |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-Flight Checklist
|
||||||
|
|
||||||
|
Before running, verify:
|
||||||
|
|
||||||
|
- [ ] **Environment**: `conda activate atomizer`
|
||||||
|
- [ ] **Config exists**: `1_setup/optimization_config.json`
|
||||||
|
- [ ] **Script exists**: `run_optimization.py`
|
||||||
|
- [ ] **Model files**: NX files in `1_setup/model/`
|
||||||
|
- [ ] **No conflicts**: No other optimization running on same study
|
||||||
|
- [ ] **Disk space**: Sufficient for results
|
||||||
|
|
||||||
|
**Quick Validation**:
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --test
|
||||||
|
```
|
||||||
|
This runs a single trial to verify setup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Modes
|
||||||
|
|
||||||
|
### 1. Standard Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python run_optimization.py
|
||||||
|
```
|
||||||
|
Uses settings from `optimization_config.json`.
|
||||||
|
|
||||||
|
### 2. Override Trials
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --n-trials 100
|
||||||
|
```
|
||||||
|
Override trial count from config.
|
||||||
|
|
||||||
|
### 3. Resume Interrupted
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --resume
|
||||||
|
```
|
||||||
|
Continues from last completed trial.
|
||||||
|
|
||||||
|
### 4. Neural Acceleration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --neural
|
||||||
|
```
|
||||||
|
Requires trained surrogate model.
|
||||||
|
|
||||||
|
### 5. Export Training Data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --export-training
|
||||||
|
```
|
||||||
|
Saves BDF/OP2 for neural network training.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring Progress
|
||||||
|
|
||||||
|
### Option 1: Console Output
|
||||||
|
The script prints progress:
|
||||||
|
```
|
||||||
|
Trial 15/50 complete. Best: 0.234 kg
|
||||||
|
Trial 16/50 complete. Best: 0.234 kg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Dashboard
|
||||||
|
See [SYS_13_DASHBOARD_TRACKING](../system/SYS_13_DASHBOARD_TRACKING.md).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start dashboard (separate terminal)
|
||||||
|
cd atomizer-dashboard/backend && python -m uvicorn api.main:app --port 8000
|
||||||
|
cd atomizer-dashboard/frontend && npm run dev
|
||||||
|
|
||||||
|
# Open browser
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Query Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "
|
||||||
|
import optuna
|
||||||
|
study = optuna.load_study('study_name', 'sqlite:///2_results/study.db')
|
||||||
|
print(f'Trials: {len(study.trials)}')
|
||||||
|
print(f'Best value: {study.best_value}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 4: Optuna Dashboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
optuna-dashboard sqlite:///2_results/study.db
|
||||||
|
# Open http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## During Execution
|
||||||
|
|
||||||
|
### What Happens Per Trial
|
||||||
|
|
||||||
|
1. **Sample parameters**: Optuna suggests design variable values
|
||||||
|
2. **Update model**: NX expressions updated via journal
|
||||||
|
3. **Solve**: NX Nastran runs FEA simulation
|
||||||
|
4. **Extract results**: Extractors read OP2 file
|
||||||
|
5. **Evaluate**: Check constraints, compute objectives
|
||||||
|
6. **Record**: Trial stored in Optuna database
|
||||||
|
|
||||||
|
### Normal Output
|
||||||
|
|
||||||
|
```
|
||||||
|
[2025-12-05 10:15:30] Trial 1 started
|
||||||
|
[2025-12-05 10:17:45] NX solve complete (135.2s)
|
||||||
|
[2025-12-05 10:17:46] Extraction complete
|
||||||
|
[2025-12-05 10:17:46] Trial 1 complete: mass=0.342 kg, stress=198.5 MPa
|
||||||
|
|
||||||
|
[2025-12-05 10:17:47] Trial 2 started
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Timing
|
||||||
|
|
||||||
|
| Operation | Typical Time |
|
||||||
|
|-----------|--------------|
|
||||||
|
| NX solve | 30s - 30min |
|
||||||
|
| Extraction | <1s |
|
||||||
|
| Per trial total | 1-30 min |
|
||||||
|
| 50 trials | 1-24 hours |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handling Issues
|
||||||
|
|
||||||
|
### Trial Failed / Pruned
|
||||||
|
|
||||||
|
```
|
||||||
|
[WARNING] Trial 12 pruned: Stress constraint violated (312.5 MPa > 250 MPa)
|
||||||
|
```
|
||||||
|
**Normal behavior** - optimizer learns from failures.
|
||||||
|
|
||||||
|
### NX Session Timeout
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] NX session timeout after 600s
|
||||||
|
```
|
||||||
|
**Solution**: Increase timeout in config or simplify model.
|
||||||
|
|
||||||
|
### Expression Not Found
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] Expression 'thicknes' not found in model
|
||||||
|
```
|
||||||
|
**Solution**: Check spelling, verify expression exists in NX.
|
||||||
|
|
||||||
|
### OP2 File Missing
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] OP2 file not found: model.op2
|
||||||
|
```
|
||||||
|
**Solution**: Check NX solve completed. Review NX log file.
|
||||||
|
|
||||||
|
### Database Locked
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] Database is locked
|
||||||
|
```
|
||||||
|
**Solution**: Another process using database. Wait or kill stale process.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stopping and Resuming
|
||||||
|
|
||||||
|
### Graceful Stop
|
||||||
|
Press `Ctrl+C` once. Current trial completes, then exits.
|
||||||
|
|
||||||
|
### Force Stop
|
||||||
|
Press `Ctrl+C` twice. Immediate exit (may lose current trial).
|
||||||
|
|
||||||
|
### Resume
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --resume
|
||||||
|
```
|
||||||
|
Continues from last completed trial. Same study database used.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Run Actions
|
||||||
|
|
||||||
|
After optimization completes:
|
||||||
|
|
||||||
|
1. **Check results**:
|
||||||
|
```bash
|
||||||
|
python -c "import optuna; s=optuna.load_study(...); print(s.best_params)"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **View in dashboard**: `http://localhost:3000`
|
||||||
|
|
||||||
|
3. **Generate report**: See [OP_04_ANALYZE_RESULTS](./OP_04_ANALYZE_RESULTS.md)
|
||||||
|
|
||||||
|
4. **Update STUDY_REPORT.md**: Fill in results template
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Integration
|
||||||
|
|
||||||
|
### With Protocol 10 (IMSO)
|
||||||
|
If enabled, optimization runs in two phases:
|
||||||
|
1. Characterization (10-30 trials)
|
||||||
|
2. Optimization (remaining trials)
|
||||||
|
|
||||||
|
Dashboard shows phase transitions.
|
||||||
|
|
||||||
|
### With Protocol 11 (Multi-Objective)
|
||||||
|
If 2+ objectives, uses NSGA-II. Returns Pareto front, not single best.
|
||||||
|
|
||||||
|
### With Protocol 13 (Dashboard)
|
||||||
|
Writes `optimizer_state.json` every trial for real-time updates.
|
||||||
|
|
||||||
|
### With Protocol 14 (Neural)
|
||||||
|
If `--neural` flag, uses trained surrogate for fast evaluation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| "ModuleNotFoundError" | Wrong environment | `conda activate atomizer` |
|
||||||
|
| All trials pruned | Constraints too tight | Relax constraints |
|
||||||
|
| Very slow | Model too complex | Simplify mesh, increase timeout |
|
||||||
|
| No improvement | Wrong sampler | Try different algorithm |
|
||||||
|
| "NX license error" | License unavailable | Check NX license server |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Preceded By**: [OP_01_CREATE_STUDY](./OP_01_CREATE_STUDY.md)
|
||||||
|
- **Followed By**: [OP_03_MONITOR_PROGRESS](./OP_03_MONITOR_PROGRESS.md), [OP_04_ANALYZE_RESULTS](./OP_04_ANALYZE_RESULTS.md)
|
||||||
|
- **Integrates With**: [SYS_10_IMSO](../system/SYS_10_IMSO.md), [SYS_13_DASHBOARD_TRACKING](../system/SYS_13_DASHBOARD_TRACKING.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial release |
|
||||||
246
docs/protocols/operations/OP_03_MONITOR_PROGRESS.md
Normal file
246
docs/protocols/operations/OP_03_MONITOR_PROGRESS.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# OP_03: Monitor Progress
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Monitor Optimization Progress
|
||||||
|
LAYER: Operations
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: user
|
||||||
|
LOAD_WITH: [SYS_13_DASHBOARD_TRACKING]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This protocol covers monitoring optimization progress through console output, dashboard, database queries, and Optuna's built-in tools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| "status", "progress" | Follow this protocol |
|
||||||
|
| "how many trials" | Query database |
|
||||||
|
| "what's happening" | Check console or dashboard |
|
||||||
|
| "is it running" | Check process status |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Method | Command/URL | Best For |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Console | Watch terminal output | Quick check |
|
||||||
|
| Dashboard | `http://localhost:3000` | Visual monitoring |
|
||||||
|
| Database query | Python one-liner | Scripted checks |
|
||||||
|
| Optuna Dashboard | `http://localhost:8080` | Detailed analysis |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring Methods
|
||||||
|
|
||||||
|
### 1. Console Output
|
||||||
|
|
||||||
|
If running in foreground, watch terminal:
|
||||||
|
```
|
||||||
|
[10:15:30] Trial 15/50 started
|
||||||
|
[10:17:45] Trial 15/50 complete: mass=0.234 kg (best: 0.212 kg)
|
||||||
|
[10:17:46] Trial 16/50 started
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Atomizer Dashboard
|
||||||
|
|
||||||
|
**Start Dashboard** (if not running):
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Backend
|
||||||
|
cd atomizer-dashboard/backend
|
||||||
|
python -m uvicorn api.main:app --reload --port 8000
|
||||||
|
|
||||||
|
# Terminal 2: Frontend
|
||||||
|
cd atomizer-dashboard/frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**View at**: `http://localhost:3000`
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Real-time trial progress bar
|
||||||
|
- Current optimizer phase (if Protocol 10)
|
||||||
|
- Pareto front visualization (if multi-objective)
|
||||||
|
- Parallel coordinates plot
|
||||||
|
- Convergence chart
|
||||||
|
|
||||||
|
### 3. Database Query
|
||||||
|
|
||||||
|
**Quick status**:
|
||||||
|
```bash
|
||||||
|
python -c "
|
||||||
|
import optuna
|
||||||
|
study = optuna.load_study(
|
||||||
|
study_name='my_study',
|
||||||
|
storage='sqlite:///studies/my_study/2_results/study.db'
|
||||||
|
)
|
||||||
|
print(f'Trials completed: {len(study.trials)}')
|
||||||
|
print(f'Best value: {study.best_value}')
|
||||||
|
print(f'Best params: {study.best_params}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detailed status**:
|
||||||
|
```python
|
||||||
|
import optuna
|
||||||
|
|
||||||
|
study = optuna.load_study(
|
||||||
|
study_name='my_study',
|
||||||
|
storage='sqlite:///studies/my_study/2_results/study.db'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Trial counts by state
|
||||||
|
from collections import Counter
|
||||||
|
states = Counter(t.state.name for t in study.trials)
|
||||||
|
print(f"Complete: {states.get('COMPLETE', 0)}")
|
||||||
|
print(f"Pruned: {states.get('PRUNED', 0)}")
|
||||||
|
print(f"Failed: {states.get('FAIL', 0)}")
|
||||||
|
print(f"Running: {states.get('RUNNING', 0)}")
|
||||||
|
|
||||||
|
# Best trials
|
||||||
|
if len(study.directions) > 1:
|
||||||
|
print(f"Pareto front size: {len(study.best_trials)}")
|
||||||
|
else:
|
||||||
|
print(f"Best value: {study.best_value}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Optuna Dashboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
optuna-dashboard sqlite:///studies/my_study/2_results/study.db
|
||||||
|
# Open http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Trial history table
|
||||||
|
- Parameter importance
|
||||||
|
- Optimization history plot
|
||||||
|
- Slice plot (parameter vs objective)
|
||||||
|
|
||||||
|
### 5. Check Running Processes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux/Mac
|
||||||
|
ps aux | grep run_optimization
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
tasklist | findstr python
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Metrics to Monitor
|
||||||
|
|
||||||
|
### Trial Progress
|
||||||
|
- Completed trials vs target
|
||||||
|
- Completion rate (trials/hour)
|
||||||
|
- Estimated time remaining
|
||||||
|
|
||||||
|
### Objective Improvement
|
||||||
|
- Current best value
|
||||||
|
- Improvement trend
|
||||||
|
- Plateau detection
|
||||||
|
|
||||||
|
### Constraint Satisfaction
|
||||||
|
- Feasibility rate (% passing constraints)
|
||||||
|
- Most violated constraint
|
||||||
|
|
||||||
|
### For Protocol 10 (IMSO)
|
||||||
|
- Current phase (Characterization vs Optimization)
|
||||||
|
- Current strategy (TPE, GP, CMA-ES)
|
||||||
|
- Characterization confidence
|
||||||
|
|
||||||
|
### For Protocol 11 (Multi-Objective)
|
||||||
|
- Pareto front size
|
||||||
|
- Hypervolume indicator
|
||||||
|
- Spread of solutions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interpreting Results
|
||||||
|
|
||||||
|
### Healthy Optimization
|
||||||
|
```
|
||||||
|
Trial 45/50: mass=0.198 kg (best: 0.195 kg)
|
||||||
|
Feasibility rate: 78%
|
||||||
|
```
|
||||||
|
- Progress toward target
|
||||||
|
- Reasonable feasibility rate (60-90%)
|
||||||
|
- Gradual improvement
|
||||||
|
|
||||||
|
### Potential Issues
|
||||||
|
|
||||||
|
**All Trials Pruned**:
|
||||||
|
```
|
||||||
|
Trial 20 pruned: constraint violated
|
||||||
|
Trial 21 pruned: constraint violated
|
||||||
|
...
|
||||||
|
```
|
||||||
|
→ Constraints too tight. Consider relaxing.
|
||||||
|
|
||||||
|
**No Improvement**:
|
||||||
|
```
|
||||||
|
Trial 30: best=0.234 (unchanged since trial 8)
|
||||||
|
Trial 31: best=0.234 (unchanged since trial 8)
|
||||||
|
```
|
||||||
|
→ May have converged, or stuck in local minimum.
|
||||||
|
|
||||||
|
**High Failure Rate**:
|
||||||
|
```
|
||||||
|
Failed: 15/50 (30%)
|
||||||
|
```
|
||||||
|
→ Model issues. Check NX logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Real-Time State File
|
||||||
|
|
||||||
|
If using Protocol 10, check:
|
||||||
|
```bash
|
||||||
|
cat studies/my_study/2_results/intelligent_optimizer/optimizer_state.json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2025-12-05T10:15:30",
|
||||||
|
"trial_number": 29,
|
||||||
|
"total_trials": 50,
|
||||||
|
"current_phase": "adaptive_optimization",
|
||||||
|
"current_strategy": "GP_UCB",
|
||||||
|
"is_multi_objective": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| Dashboard shows old data | Backend not running | Start backend |
|
||||||
|
| "No study found" | Wrong path | Check study name and path |
|
||||||
|
| Trial count not increasing | Process stopped | Check if still running |
|
||||||
|
| Dashboard not updating | Polling issue | Refresh browser |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Preceded By**: [OP_02_RUN_OPTIMIZATION](./OP_02_RUN_OPTIMIZATION.md)
|
||||||
|
- **Followed By**: [OP_04_ANALYZE_RESULTS](./OP_04_ANALYZE_RESULTS.md)
|
||||||
|
- **Integrates With**: [SYS_13_DASHBOARD_TRACKING](../system/SYS_13_DASHBOARD_TRACKING.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial release |
|
||||||
302
docs/protocols/operations/OP_04_ANALYZE_RESULTS.md
Normal file
302
docs/protocols/operations/OP_04_ANALYZE_RESULTS.md
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
# OP_04: Analyze Results
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Analyze Optimization Results
|
||||||
|
LAYER: Operations
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: user
|
||||||
|
LOAD_WITH: []
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This protocol covers analyzing optimization results, including extracting best solutions, generating reports, comparing designs, and interpreting Pareto fronts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| "results", "what did we find" | Follow this protocol |
|
||||||
|
| "best design" | Extract best trial |
|
||||||
|
| "compare", "trade-off" | Pareto analysis |
|
||||||
|
| "report" | Generate summary |
|
||||||
|
| Optimization complete | Analyze and document |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Key Outputs**:
|
||||||
|
| Output | Location | Purpose |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| Best parameters | `study.best_params` | Optimal design |
|
||||||
|
| Pareto front | `study.best_trials` | Trade-off solutions |
|
||||||
|
| Trial history | `study.trials` | Full exploration |
|
||||||
|
| Intelligence report | `intelligent_optimizer/` | Algorithm insights |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Analysis Methods
|
||||||
|
|
||||||
|
### 1. Single-Objective Results
|
||||||
|
|
||||||
|
```python
|
||||||
|
import optuna
|
||||||
|
|
||||||
|
study = optuna.load_study(
|
||||||
|
study_name='my_study',
|
||||||
|
storage='sqlite:///2_results/study.db'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Best result
|
||||||
|
print(f"Best value: {study.best_value}")
|
||||||
|
print(f"Best parameters: {study.best_params}")
|
||||||
|
print(f"Best trial: #{study.best_trial.number}")
|
||||||
|
|
||||||
|
# Get full best trial details
|
||||||
|
best = study.best_trial
|
||||||
|
print(f"User attributes: {best.user_attrs}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Multi-Objective Results (Pareto Front)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import optuna
|
||||||
|
|
||||||
|
study = optuna.load_study(
|
||||||
|
study_name='my_study',
|
||||||
|
storage='sqlite:///2_results/study.db'
|
||||||
|
)
|
||||||
|
|
||||||
|
# All Pareto-optimal solutions
|
||||||
|
pareto_trials = study.best_trials
|
||||||
|
print(f"Pareto front size: {len(pareto_trials)}")
|
||||||
|
|
||||||
|
# Print all Pareto solutions
|
||||||
|
for trial in pareto_trials:
|
||||||
|
print(f"Trial {trial.number}: {trial.values} - {trial.params}")
|
||||||
|
|
||||||
|
# Find extremes
|
||||||
|
# Assuming objectives: [stiffness (max), mass (min)]
|
||||||
|
best_stiffness = max(pareto_trials, key=lambda t: t.values[0])
|
||||||
|
lightest = min(pareto_trials, key=lambda t: t.values[1])
|
||||||
|
|
||||||
|
print(f"Best stiffness: Trial {best_stiffness.number}")
|
||||||
|
print(f"Lightest: Trial {lightest.number}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Parameter Importance
|
||||||
|
|
||||||
|
```python
|
||||||
|
import optuna
|
||||||
|
|
||||||
|
study = optuna.load_study(...)
|
||||||
|
|
||||||
|
# Parameter importance (which parameters matter most)
|
||||||
|
importance = optuna.importance.get_param_importances(study)
|
||||||
|
for param, score in importance.items():
|
||||||
|
print(f"{param}: {score:.3f}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Constraint Analysis
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Find feasibility rate
|
||||||
|
completed = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]
|
||||||
|
pruned = [t for t in study.trials if t.state == optuna.trial.TrialState.PRUNED]
|
||||||
|
|
||||||
|
feasibility_rate = len(completed) / (len(completed) + len(pruned))
|
||||||
|
print(f"Feasibility rate: {feasibility_rate:.1%}")
|
||||||
|
|
||||||
|
# Analyze why trials were pruned
|
||||||
|
for trial in pruned[:5]: # First 5 pruned
|
||||||
|
reason = trial.user_attrs.get('pruning_reason', 'Unknown')
|
||||||
|
print(f"Trial {trial.number}: {reason}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Visualization
|
||||||
|
|
||||||
|
### Using Optuna Dashboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
optuna-dashboard sqlite:///2_results/study.db
|
||||||
|
# Open http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available Plots**:
|
||||||
|
- Optimization history
|
||||||
|
- Parameter importance
|
||||||
|
- Slice plot (parameter vs objective)
|
||||||
|
- Parallel coordinates
|
||||||
|
- Contour plot (2D parameter interaction)
|
||||||
|
|
||||||
|
### Using Atomizer Dashboard
|
||||||
|
|
||||||
|
Navigate to `http://localhost:3000` and select study.
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Pareto front plot with normalization
|
||||||
|
- Parallel coordinates with selection
|
||||||
|
- Real-time convergence chart
|
||||||
|
|
||||||
|
### Custom Visualization
|
||||||
|
|
||||||
|
```python
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import optuna
|
||||||
|
|
||||||
|
study = optuna.load_study(...)
|
||||||
|
|
||||||
|
# Plot optimization history
|
||||||
|
fig = optuna.visualization.plot_optimization_history(study)
|
||||||
|
fig.show()
|
||||||
|
|
||||||
|
# Plot parameter importance
|
||||||
|
fig = optuna.visualization.plot_param_importances(study)
|
||||||
|
fig.show()
|
||||||
|
|
||||||
|
# Plot Pareto front (multi-objective)
|
||||||
|
if len(study.directions) > 1:
|
||||||
|
fig = optuna.visualization.plot_pareto_front(study)
|
||||||
|
fig.show()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generate Reports
|
||||||
|
|
||||||
|
### Update STUDY_REPORT.md
|
||||||
|
|
||||||
|
After analysis, fill in the template:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Study Report: bracket_optimization
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
- **Trials completed**: 50
|
||||||
|
- **Best mass**: 0.195 kg
|
||||||
|
- **Best parameters**: thickness=4.2mm, width=25.8mm
|
||||||
|
- **Constraint satisfaction**: All constraints met
|
||||||
|
|
||||||
|
## Optimization Progress
|
||||||
|
- Initial best: 0.342 kg (trial 1)
|
||||||
|
- Final best: 0.195 kg (trial 38)
|
||||||
|
- Improvement: 43%
|
||||||
|
|
||||||
|
## Best Designs Found
|
||||||
|
|
||||||
|
### Design 1 (Overall Best)
|
||||||
|
| Parameter | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| thickness | 4.2 mm |
|
||||||
|
| width | 25.8 mm |
|
||||||
|
|
||||||
|
| Metric | Value | Constraint |
|
||||||
|
|--------|-------|------------|
|
||||||
|
| Mass | 0.195 kg | - |
|
||||||
|
| Max stress | 238.5 MPa | < 250 MPa ✓ |
|
||||||
|
|
||||||
|
## Engineering Recommendations
|
||||||
|
1. Recommended design: Trial 38 parameters
|
||||||
|
2. Safety margin: 4.6% on stress constraint
|
||||||
|
3. Consider manufacturing tolerance analysis
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export to CSV
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# All trials to DataFrame
|
||||||
|
trials_data = []
|
||||||
|
for trial in study.trials:
|
||||||
|
if trial.state == optuna.trial.TrialState.COMPLETE:
|
||||||
|
row = {'trial': trial.number, 'value': trial.value}
|
||||||
|
row.update(trial.params)
|
||||||
|
trials_data.append(row)
|
||||||
|
|
||||||
|
df = pd.DataFrame(trials_data)
|
||||||
|
df.to_csv('optimization_results.csv', index=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Best Design for FEA Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get best parameters
|
||||||
|
best_params = study.best_params
|
||||||
|
|
||||||
|
# Format for NX expression update
|
||||||
|
for name, value in best_params.items():
|
||||||
|
print(f"{name} = {value}")
|
||||||
|
|
||||||
|
# Or save as JSON
|
||||||
|
import json
|
||||||
|
with open('best_design.json', 'w') as f:
|
||||||
|
json.dump(best_params, f, indent=2)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intelligence Report (Protocol 10)
|
||||||
|
|
||||||
|
If using Protocol 10, check intelligence files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Landscape analysis
|
||||||
|
cat 2_results/intelligent_optimizer/intelligence_report.json
|
||||||
|
|
||||||
|
# Characterization progress
|
||||||
|
cat 2_results/intelligent_optimizer/characterization_progress.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Insights**:
|
||||||
|
- Landscape classification (smooth/rugged, unimodal/multimodal)
|
||||||
|
- Algorithm recommendation rationale
|
||||||
|
- Parameter correlations
|
||||||
|
- Confidence metrics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Checklist
|
||||||
|
|
||||||
|
Before finalizing results:
|
||||||
|
|
||||||
|
- [ ] Best solution satisfies all constraints
|
||||||
|
- [ ] Results are physically reasonable
|
||||||
|
- [ ] Parameter values within manufacturing limits
|
||||||
|
- [ ] Consider re-running FEA on best design to confirm
|
||||||
|
- [ ] Document any anomalies or surprises
|
||||||
|
- [ ] Update STUDY_REPORT.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| Best value seems wrong | Constraint not enforced | Check objective function |
|
||||||
|
| No Pareto solutions | All trials failed | Check constraints |
|
||||||
|
| Unexpected best params | Local minimum | Try different starting points |
|
||||||
|
| Can't load study | Wrong path | Verify database location |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Preceded By**: [OP_02_RUN_OPTIMIZATION](./OP_02_RUN_OPTIMIZATION.md), [OP_03_MONITOR_PROGRESS](./OP_03_MONITOR_PROGRESS.md)
|
||||||
|
- **Related**: [SYS_11_MULTI_OBJECTIVE](../system/SYS_11_MULTI_OBJECTIVE.md) for Pareto analysis
|
||||||
|
- **Skill**: `.claude/skills/generate-report.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial release |
|
||||||
294
docs/protocols/operations/OP_05_EXPORT_TRAINING_DATA.md
Normal file
294
docs/protocols/operations/OP_05_EXPORT_TRAINING_DATA.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# OP_05: Export Training Data
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Export Neural Network Training Data
|
||||||
|
LAYER: Operations
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: user
|
||||||
|
LOAD_WITH: [SYS_14_NEURAL_ACCELERATION]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This protocol covers exporting FEA simulation data for training neural network surrogates. Proper data export enables Protocol 14 (Neural Acceleration).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| "export training data" | Follow this protocol |
|
||||||
|
| "neural network data" | Follow this protocol |
|
||||||
|
| Planning >50 trials | Consider export for acceleration |
|
||||||
|
| Want to train surrogate | Follow this protocol |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Export Command**:
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --export-training
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output Structure**:
|
||||||
|
```
|
||||||
|
atomizer_field_training_data/{study_name}/
|
||||||
|
├── trial_0001/
|
||||||
|
│ ├── input/model.bdf
|
||||||
|
│ ├── output/model.op2
|
||||||
|
│ └── metadata.json
|
||||||
|
├── trial_0002/
|
||||||
|
│ └── ...
|
||||||
|
└── study_summary.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Data Volume**:
|
||||||
|
| Complexity | Training Samples | Validation Samples |
|
||||||
|
|------------|-----------------|-------------------|
|
||||||
|
| Simple (2-3 params) | 50-100 | 20-30 |
|
||||||
|
| Medium (4-6 params) | 100-200 | 30-50 |
|
||||||
|
| Complex (7+ params) | 200-500 | 50-100 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Enable Export in Config
|
||||||
|
|
||||||
|
Add to `optimization_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"training_data_export": {
|
||||||
|
"enabled": true,
|
||||||
|
"export_dir": "atomizer_field_training_data/my_study",
|
||||||
|
"export_bdf": true,
|
||||||
|
"export_op2": true,
|
||||||
|
"export_fields": ["displacement", "stress"],
|
||||||
|
"include_failed": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | false | Enable export |
|
||||||
|
| `export_dir` | string | - | Output directory |
|
||||||
|
| `export_bdf` | bool | true | Save Nastran input |
|
||||||
|
| `export_op2` | bool | true | Save binary results |
|
||||||
|
| `export_fields` | list | all | Which result fields |
|
||||||
|
| `include_failed` | bool | false | Include failed trials |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Export Workflow
|
||||||
|
|
||||||
|
### Step 1: Run with Export Enabled
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda activate atomizer
|
||||||
|
cd studies/my_study
|
||||||
|
python run_optimization.py --export-training
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run standard optimization with config export enabled.
|
||||||
|
|
||||||
|
### Step 2: Verify Export
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls atomizer_field_training_data/my_study/
|
||||||
|
# Should see trial_0001/, trial_0002/, etc.
|
||||||
|
|
||||||
|
# Check a trial
|
||||||
|
ls atomizer_field_training_data/my_study/trial_0001/
|
||||||
|
# input/model.bdf
|
||||||
|
# output/model.op2
|
||||||
|
# metadata.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Check Metadata
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat atomizer_field_training_data/my_study/trial_0001/metadata.json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"trial_number": 1,
|
||||||
|
"design_parameters": {
|
||||||
|
"thickness": 5.2,
|
||||||
|
"width": 30.0
|
||||||
|
},
|
||||||
|
"objectives": {
|
||||||
|
"mass": 0.234,
|
||||||
|
"max_stress": 198.5
|
||||||
|
},
|
||||||
|
"constraints_satisfied": true,
|
||||||
|
"simulation_time": 145.2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Check Study Summary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat atomizer_field_training_data/my_study/study_summary.json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"study_name": "my_study",
|
||||||
|
"total_trials": 50,
|
||||||
|
"successful_exports": 47,
|
||||||
|
"failed_exports": 3,
|
||||||
|
"design_parameters": ["thickness", "width"],
|
||||||
|
"objectives": ["mass", "max_stress"],
|
||||||
|
"export_timestamp": "2025-12-05T15:30:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Quality Checks
|
||||||
|
|
||||||
|
### Verify Sample Count
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
export_dir = Path("atomizer_field_training_data/my_study")
|
||||||
|
trials = list(export_dir.glob("trial_*"))
|
||||||
|
print(f"Exported trials: {len(trials)}")
|
||||||
|
|
||||||
|
# Check for missing files
|
||||||
|
for trial_dir in trials:
|
||||||
|
bdf = trial_dir / "input" / "model.bdf"
|
||||||
|
op2 = trial_dir / "output" / "model.op2"
|
||||||
|
meta = trial_dir / "metadata.json"
|
||||||
|
|
||||||
|
if not all([bdf.exists(), op2.exists(), meta.exists()]):
|
||||||
|
print(f"Missing files in {trial_dir}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Parameter Coverage
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Load all metadata
|
||||||
|
params = []
|
||||||
|
for trial_dir in export_dir.glob("trial_*"):
|
||||||
|
with open(trial_dir / "metadata.json") as f:
|
||||||
|
meta = json.load(f)
|
||||||
|
params.append(meta["design_parameters"])
|
||||||
|
|
||||||
|
# Check coverage
|
||||||
|
import pandas as pd
|
||||||
|
df = pd.DataFrame(params)
|
||||||
|
print(df.describe())
|
||||||
|
|
||||||
|
# Look for gaps
|
||||||
|
for col in df.columns:
|
||||||
|
print(f"{col}: min={df[col].min():.2f}, max={df[col].max():.2f}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Space-Filling Sampling
|
||||||
|
|
||||||
|
For best neural network training, use space-filling designs:
|
||||||
|
|
||||||
|
### Latin Hypercube Sampling
|
||||||
|
|
||||||
|
```python
|
||||||
|
from scipy.stats import qmc
|
||||||
|
|
||||||
|
# Generate space-filling samples
|
||||||
|
n_samples = 100
|
||||||
|
n_params = 4
|
||||||
|
|
||||||
|
sampler = qmc.LatinHypercube(d=n_params)
|
||||||
|
samples = sampler.random(n=n_samples)
|
||||||
|
|
||||||
|
# Scale to parameter bounds
|
||||||
|
lower = [2.0, 20.0, 5.0, 1.0]
|
||||||
|
upper = [10.0, 50.0, 15.0, 5.0]
|
||||||
|
scaled = qmc.scale(samples, lower, upper)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sobol Sequence
|
||||||
|
|
||||||
|
```python
|
||||||
|
sampler = qmc.Sobol(d=n_params)
|
||||||
|
samples = sampler.random(n=n_samples)
|
||||||
|
scaled = qmc.scale(samples, lower, upper)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps After Export
|
||||||
|
|
||||||
|
### 1. Parse to Neural Format
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd atomizer-field
|
||||||
|
python batch_parser.py ../atomizer_field_training_data/my_study
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Split Train/Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
|
||||||
|
# 80/20 split
|
||||||
|
train_trials, val_trials = train_test_split(
|
||||||
|
all_trials,
|
||||||
|
test_size=0.2,
|
||||||
|
random_state=42
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Train Model
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python train_parametric.py \
|
||||||
|
--train_dir ../training_data/parsed \
|
||||||
|
--val_dir ../validation_data/parsed \
|
||||||
|
--epochs 200
|
||||||
|
```
|
||||||
|
|
||||||
|
See [SYS_14_NEURAL_ACCELERATION](../system/SYS_14_NEURAL_ACCELERATION.md) for full training workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| No export directory | Export not enabled | Add `training_data_export` to config |
|
||||||
|
| Missing OP2 files | Solve failed | Check `include_failed: false` |
|
||||||
|
| Incomplete metadata | Extraction error | Check extractor logs |
|
||||||
|
| Low sample count | Too many failures | Relax constraints |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Related**: [SYS_14_NEURAL_ACCELERATION](../system/SYS_14_NEURAL_ACCELERATION.md)
|
||||||
|
- **Preceded By**: [OP_02_RUN_OPTIMIZATION](./OP_02_RUN_OPTIMIZATION.md)
|
||||||
|
- **Skill**: `.claude/skills/modules/neural-acceleration.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial release |
|
||||||
437
docs/protocols/operations/OP_06_TROUBLESHOOT.md
Normal file
437
docs/protocols/operations/OP_06_TROUBLESHOOT.md
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
# OP_06: Troubleshoot
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Troubleshoot Optimization Issues
|
||||||
|
LAYER: Operations
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: user
|
||||||
|
LOAD_WITH: []
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This protocol provides systematic troubleshooting for common optimization issues, covering NX errors, extraction failures, database problems, and performance issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| "error", "failed" | Follow this protocol |
|
||||||
|
| "not working", "crashed" | Follow this protocol |
|
||||||
|
| "help", "stuck" | Follow this protocol |
|
||||||
|
| Unexpected behavior | Follow this protocol |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Diagnostic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Check environment
|
||||||
|
conda activate atomizer
|
||||||
|
python --version # Should be 3.9+
|
||||||
|
|
||||||
|
# 2. Check study structure
|
||||||
|
ls studies/my_study/
|
||||||
|
# Should have: 1_setup/, run_optimization.py
|
||||||
|
|
||||||
|
# 3. Check model files
|
||||||
|
ls studies/my_study/1_setup/model/
|
||||||
|
# Should have: .prt, .sim files
|
||||||
|
|
||||||
|
# 4. Test single trial
|
||||||
|
python run_optimization.py --test
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Categories
|
||||||
|
|
||||||
|
### 1. Environment Errors
|
||||||
|
|
||||||
|
#### "ModuleNotFoundError: No module named 'optuna'"
|
||||||
|
|
||||||
|
**Cause**: Wrong Python environment
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
conda activate atomizer
|
||||||
|
# Verify
|
||||||
|
conda list | grep optuna
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "Python version mismatch"
|
||||||
|
|
||||||
|
**Cause**: Wrong Python version
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
python --version # Need 3.9+
|
||||||
|
conda activate atomizer
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. NX Model Setup Errors
|
||||||
|
|
||||||
|
#### "All optimization trials produce identical results"
|
||||||
|
|
||||||
|
**Cause**: Missing idealized part (`*_i.prt`) or broken file chain
|
||||||
|
|
||||||
|
**Symptoms**:
|
||||||
|
- Journal shows "FE model updated" but results don't change
|
||||||
|
- DAT files have same node coordinates with different expressions
|
||||||
|
- OP2 file timestamps update but values are identical
|
||||||
|
|
||||||
|
**Root Cause**: NX simulation files have a parent-child hierarchy:
|
||||||
|
```
|
||||||
|
.sim → .fem → _i.prt → .prt (geometry)
|
||||||
|
```
|
||||||
|
|
||||||
|
If the `_i.prt` (idealized part) is missing or not properly linked, `UpdateFemodel()` runs but the mesh doesn't regenerate because:
|
||||||
|
- FEM mesh is tied to idealized geometry, not master geometry
|
||||||
|
- Without idealized part updating, FEM has nothing new to mesh against
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. **Check file chain in NX**:
|
||||||
|
- Open `.sim` file
|
||||||
|
- Go to **Part Navigator** or **Assembly Navigator**
|
||||||
|
- List ALL referenced parts
|
||||||
|
|
||||||
|
2. **Copy ALL linked files** to study folder:
|
||||||
|
```bash
|
||||||
|
# Typical file set needed:
|
||||||
|
Model.prt # Geometry
|
||||||
|
Model_fem1_i.prt # Idealized part ← OFTEN MISSING!
|
||||||
|
Model_fem1.fem # FEM file
|
||||||
|
Model_sim1.sim # Simulation file
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify links are intact**:
|
||||||
|
- Open model in NX after copying
|
||||||
|
- Check that updates propagate: Geometry → Idealized → FEM → Sim
|
||||||
|
|
||||||
|
4. **CRITICAL CODE FIX** (already implemented in `solve_simulation.py`):
|
||||||
|
The idealized part MUST be explicitly loaded before `UpdateFemodel()`:
|
||||||
|
```python
|
||||||
|
# Load idealized part BEFORE updating FEM
|
||||||
|
for filename in os.listdir(working_dir):
|
||||||
|
if '_i.prt' in filename.lower():
|
||||||
|
idealized_part, status = theSession.Parts.Open(path)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Now UpdateFemodel() will work correctly
|
||||||
|
feModel.UpdateFemodel()
|
||||||
|
```
|
||||||
|
Without loading the `_i.prt`, NX cannot propagate geometry changes to the mesh.
|
||||||
|
|
||||||
|
**Prevention**: Always use introspection to list all parts referenced by a simulation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. NX/Solver Errors
|
||||||
|
|
||||||
|
#### "NX session timeout after 600s"
|
||||||
|
|
||||||
|
**Cause**: Model too complex or NX stuck
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Increase timeout in config:
|
||||||
|
```json
|
||||||
|
"simulation": {
|
||||||
|
"timeout": 1200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. Simplify mesh if possible
|
||||||
|
3. Check NX license availability
|
||||||
|
|
||||||
|
#### "Expression 'xxx' not found in model"
|
||||||
|
|
||||||
|
**Cause**: Expression name mismatch
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Open model in NX
|
||||||
|
2. Go to Tools → Expressions
|
||||||
|
3. Verify exact expression name (case-sensitive)
|
||||||
|
4. Update config to match
|
||||||
|
|
||||||
|
#### "NX license error"
|
||||||
|
|
||||||
|
**Cause**: License server unavailable
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check license server status
|
||||||
|
2. Wait and retry
|
||||||
|
3. Contact IT if persistent
|
||||||
|
|
||||||
|
#### "NX solve failed - check log"
|
||||||
|
|
||||||
|
**Cause**: Nastran solver error
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Find log file: `1_setup/model/*.log` or `*.f06`
|
||||||
|
2. Search for "FATAL" or "ERROR"
|
||||||
|
3. Common causes:
|
||||||
|
- Singular stiffness matrix (constraints issue)
|
||||||
|
- Bad mesh (distorted elements)
|
||||||
|
- Missing material properties
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Extraction Errors
|
||||||
|
|
||||||
|
#### "OP2 file not found"
|
||||||
|
|
||||||
|
**Cause**: Solve didn't produce output
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check if solve completed
|
||||||
|
2. Look for `.op2` file in model directory
|
||||||
|
3. Check NX log for solve errors
|
||||||
|
|
||||||
|
#### "No displacement data for subcase X"
|
||||||
|
|
||||||
|
**Cause**: Wrong subcase number
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check available subcases in OP2:
|
||||||
|
```python
|
||||||
|
from pyNastran.op2.op2 import OP2
|
||||||
|
op2 = OP2()
|
||||||
|
op2.read_op2('model.op2')
|
||||||
|
print(op2.displacements.keys())
|
||||||
|
```
|
||||||
|
2. Update subcase in extractor call
|
||||||
|
|
||||||
|
#### "Element type 'xxx' not supported"
|
||||||
|
|
||||||
|
**Cause**: Extractor doesn't support element type
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check available types in extractor
|
||||||
|
2. Common types: `cquad4`, `ctria3`, `ctetra`, `chexa`
|
||||||
|
3. May need different extractor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Database Errors
|
||||||
|
|
||||||
|
#### "Database is locked"
|
||||||
|
|
||||||
|
**Cause**: Another process using database
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check for running processes:
|
||||||
|
```bash
|
||||||
|
ps aux | grep run_optimization
|
||||||
|
```
|
||||||
|
2. Kill stale process if needed
|
||||||
|
3. Wait for other optimization to finish
|
||||||
|
|
||||||
|
#### "Study 'xxx' not found"
|
||||||
|
|
||||||
|
**Cause**: Wrong study name or path
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check exact study name in database:
|
||||||
|
```python
|
||||||
|
import optuna
|
||||||
|
storage = optuna.storages.RDBStorage('sqlite:///study.db')
|
||||||
|
print(storage.get_all_study_summaries())
|
||||||
|
```
|
||||||
|
2. Use correct name when loading
|
||||||
|
|
||||||
|
#### "IntegrityError: UNIQUE constraint failed"
|
||||||
|
|
||||||
|
**Cause**: Duplicate trial number
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Don't run multiple optimizations on same study simultaneously
|
||||||
|
2. Use `--resume` flag for continuation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Constraint/Feasibility Errors
|
||||||
|
|
||||||
|
#### "All trials pruned"
|
||||||
|
|
||||||
|
**Cause**: No feasible region
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check constraint values:
|
||||||
|
```python
|
||||||
|
# In objective function, print constraint values
|
||||||
|
print(f"Stress: {stress}, limit: 250")
|
||||||
|
```
|
||||||
|
2. Relax constraints
|
||||||
|
3. Widen design variable bounds
|
||||||
|
|
||||||
|
#### "No improvement after N trials"
|
||||||
|
|
||||||
|
**Cause**: Stuck in local minimum or converged
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check if truly converged (good result)
|
||||||
|
2. Try different starting region
|
||||||
|
3. Use different sampler
|
||||||
|
4. Increase exploration (lower `n_startup_trials`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Performance Issues
|
||||||
|
|
||||||
|
#### "Trials running very slowly"
|
||||||
|
|
||||||
|
**Cause**: Complex model or inefficient extraction
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Profile time per component:
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
start = time.time()
|
||||||
|
# ... operation ...
|
||||||
|
print(f"Took: {time.time() - start:.1f}s")
|
||||||
|
```
|
||||||
|
2. Simplify mesh if NX is slow
|
||||||
|
3. Check extraction isn't re-parsing OP2 multiple times
|
||||||
|
|
||||||
|
#### "Memory error"
|
||||||
|
|
||||||
|
**Cause**: Large OP2 file or many trials
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Clear Python memory between trials
|
||||||
|
2. Don't store all results in memory
|
||||||
|
3. Use database for persistence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diagnostic Commands
|
||||||
|
|
||||||
|
### Quick Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Environment
|
||||||
|
conda activate atomizer
|
||||||
|
python -c "import optuna; print('Optuna OK')"
|
||||||
|
python -c "import pyNastran; print('pyNastran OK')"
|
||||||
|
|
||||||
|
# Study structure
|
||||||
|
ls -la studies/my_study/
|
||||||
|
|
||||||
|
# Config validity
|
||||||
|
python -c "
|
||||||
|
import json
|
||||||
|
with open('studies/my_study/1_setup/optimization_config.json') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
print('Config OK')
|
||||||
|
print(f'Objectives: {len(config.get(\"objectives\", []))}')
|
||||||
|
"
|
||||||
|
|
||||||
|
# Database status
|
||||||
|
python -c "
|
||||||
|
import optuna
|
||||||
|
study = optuna.load_study('my_study', 'sqlite:///studies/my_study/2_results/study.db')
|
||||||
|
print(f'Trials: {len(study.trials)}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### NX Log Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find latest log
|
||||||
|
ls -lt studies/my_study/1_setup/model/*.log | head -1
|
||||||
|
|
||||||
|
# Search for errors
|
||||||
|
grep -i "error\|fatal\|fail" studies/my_study/1_setup/model/*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trial Failure Analysis
|
||||||
|
|
||||||
|
```python
|
||||||
|
import optuna
|
||||||
|
|
||||||
|
study = optuna.load_study(...)
|
||||||
|
|
||||||
|
# Failed trials
|
||||||
|
failed = [t for t in study.trials
|
||||||
|
if t.state == optuna.trial.TrialState.FAIL]
|
||||||
|
print(f"Failed: {len(failed)}")
|
||||||
|
|
||||||
|
for t in failed[:5]:
|
||||||
|
print(f"Trial {t.number}: {t.user_attrs}")
|
||||||
|
|
||||||
|
# Pruned trials
|
||||||
|
pruned = [t for t in study.trials
|
||||||
|
if t.state == optuna.trial.TrialState.PRUNED]
|
||||||
|
print(f"Pruned: {len(pruned)}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recovery Actions
|
||||||
|
|
||||||
|
### Reset Study (Start Fresh)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup first
|
||||||
|
cp -r studies/my_study/2_results studies/my_study/2_results_backup
|
||||||
|
|
||||||
|
# Delete results
|
||||||
|
rm -rf studies/my_study/2_results/*
|
||||||
|
|
||||||
|
# Run fresh
|
||||||
|
python run_optimization.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resume Interrupted Study
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python run_optimization.py --resume
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore from Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r studies/my_study/2_results_backup/* studies/my_study/2_results/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
### Information to Provide
|
||||||
|
|
||||||
|
When asking for help, include:
|
||||||
|
1. Error message (full traceback)
|
||||||
|
2. Config file contents
|
||||||
|
3. Study structure (`ls -la`)
|
||||||
|
4. What you tried
|
||||||
|
5. NX log excerpt (if NX error)
|
||||||
|
|
||||||
|
### Log Locations
|
||||||
|
|
||||||
|
| Log | Location |
|
||||||
|
|-----|----------|
|
||||||
|
| Optimization | Console output or redirect to file |
|
||||||
|
| NX Solve | `1_setup/model/*.log`, `*.f06` |
|
||||||
|
| Database | `2_results/study.db` (query with optuna) |
|
||||||
|
| Intelligence | `2_results/intelligent_optimizer/*.json` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Related**: All operation protocols
|
||||||
|
- **System**: [SYS_10_IMSO](../system/SYS_10_IMSO.md), [SYS_12_EXTRACTOR_LIBRARY](../system/SYS_12_EXTRACTOR_LIBRARY.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-05 | Initial release |
|
||||||
341
docs/protocols/system/SYS_10_IMSO.md
Normal file
341
docs/protocols/system/SYS_10_IMSO.md
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
# SYS_10: Intelligent Multi-Strategy Optimization (IMSO)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Intelligent Multi-Strategy Optimization
|
||||||
|
LAYER: System
|
||||||
|
VERSION: 2.1
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: user
|
||||||
|
LOAD_WITH: []
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Protocol 10 implements adaptive optimization that automatically characterizes the problem landscape and selects the best optimization algorithm. This two-phase approach combines automated landscape analysis with algorithm-specific optimization.
|
||||||
|
|
||||||
|
**Key Innovation**: Adaptive characterization phase that intelligently determines when enough exploration has been done, then transitions to the optimal algorithm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| Single-objective optimization | Use this protocol |
|
||||||
|
| "adaptive", "intelligent", "IMSO" mentioned | Load this protocol |
|
||||||
|
| User unsure which algorithm to use | Recommend this protocol |
|
||||||
|
| Complex landscape suspected | Use this protocol |
|
||||||
|
|
||||||
|
**Do NOT use when**: Multi-objective optimization needed (use SYS_11 instead)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Parameter | Default | Range | Description |
|
||||||
|
|-----------|---------|-------|-------------|
|
||||||
|
| `min_trials` | 10 | 5-50 | Minimum characterization trials |
|
||||||
|
| `max_trials` | 30 | 10-100 | Maximum characterization trials |
|
||||||
|
| `confidence_threshold` | 0.85 | 0.0-1.0 | Stopping confidence level |
|
||||||
|
| `check_interval` | 5 | 1-10 | Trials between checks |
|
||||||
|
|
||||||
|
**Landscape → Algorithm Mapping**:
|
||||||
|
|
||||||
|
| Landscape Type | Primary Strategy | Fallback |
|
||||||
|
|----------------|------------------|----------|
|
||||||
|
| smooth_unimodal | GP-BO | CMA-ES |
|
||||||
|
| smooth_multimodal | GP-BO | TPE |
|
||||||
|
| rugged_unimodal | TPE | CMA-ES |
|
||||||
|
| rugged_multimodal | TPE | - |
|
||||||
|
| noisy | TPE | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ PHASE 1: ADAPTIVE CHARACTERIZATION STUDY │
|
||||||
|
│ ───────────────────────────────────────────────────────── │
|
||||||
|
│ Sampler: Random/Sobol (unbiased exploration) │
|
||||||
|
│ Trials: 10-30 (adapts to problem complexity) │
|
||||||
|
│ │
|
||||||
|
│ Every 5 trials: │
|
||||||
|
│ → Analyze landscape metrics │
|
||||||
|
│ → Check metric convergence │
|
||||||
|
│ → Calculate characterization confidence │
|
||||||
|
│ → Decide if ready to stop │
|
||||||
|
│ │
|
||||||
|
│ Stop when: │
|
||||||
|
│ ✓ Confidence ≥ 85% │
|
||||||
|
│ ✓ OR max trials reached (30) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ TRANSITION: LANDSCAPE ANALYSIS & STRATEGY SELECTION │
|
||||||
|
│ ───────────────────────────────────────────────────────── │
|
||||||
|
│ Analyze: │
|
||||||
|
│ - Smoothness (0-1) │
|
||||||
|
│ - Multimodality (number of modes) │
|
||||||
|
│ - Parameter correlation │
|
||||||
|
│ - Noise level │
|
||||||
|
│ │
|
||||||
|
│ Classify & Recommend: │
|
||||||
|
│ smooth_unimodal → GP-BO (best) or CMA-ES │
|
||||||
|
│ smooth_multimodal → GP-BO │
|
||||||
|
│ rugged_multimodal → TPE │
|
||||||
|
│ rugged_unimodal → TPE or CMA-ES │
|
||||||
|
│ noisy → TPE (most robust) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ PHASE 2: OPTIMIZATION STUDY │
|
||||||
|
│ ───────────────────────────────────────────────────────── │
|
||||||
|
│ Sampler: Recommended from Phase 1 │
|
||||||
|
│ Warm Start: Initialize from best characterization point │
|
||||||
|
│ Trials: User-specified (default 50) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Components
|
||||||
|
|
||||||
|
### 1. Adaptive Characterization (`adaptive_characterization.py`)
|
||||||
|
|
||||||
|
**Confidence Calculation**:
|
||||||
|
```python
|
||||||
|
confidence = (
|
||||||
|
0.40 * metric_stability_score + # Are metrics converging?
|
||||||
|
0.30 * parameter_coverage_score + # Explored enough space?
|
||||||
|
0.20 * sample_adequacy_score + # Enough samples for complexity?
|
||||||
|
0.10 * landscape_clarity_score # Clear classification?
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stopping Criteria**:
|
||||||
|
- **Minimum trials**: 10 (baseline data requirement)
|
||||||
|
- **Maximum trials**: 30 (prevent over-characterization)
|
||||||
|
- **Confidence threshold**: 85% (high confidence required)
|
||||||
|
- **Check interval**: Every 5 trials
|
||||||
|
|
||||||
|
**Adaptive Behavior**:
|
||||||
|
```python
|
||||||
|
# Simple problem (smooth, unimodal, low noise):
|
||||||
|
if smoothness > 0.6 and unimodal and noise < 0.3:
|
||||||
|
required_samples = 10 + dimensionality
|
||||||
|
# Stops at ~10-15 trials
|
||||||
|
|
||||||
|
# Complex problem (multimodal with N modes):
|
||||||
|
if multimodal and n_modes > 2:
|
||||||
|
required_samples = 10 + 5 * n_modes + 2 * dimensionality
|
||||||
|
# Continues to ~20-30 trials
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Landscape Analyzer (`landscape_analyzer.py`)
|
||||||
|
|
||||||
|
**Metrics Computed**:
|
||||||
|
|
||||||
|
| Metric | Method | Interpretation |
|
||||||
|
|--------|--------|----------------|
|
||||||
|
| Smoothness (0-1) | Spearman correlation | >0.6: Good for CMA-ES, GP-BO |
|
||||||
|
| Multimodality | DBSCAN clustering | Detects distinct good regions |
|
||||||
|
| Correlation | Parameter-objective correlation | Identifies influential params |
|
||||||
|
| Noise (0-1) | Local consistency check | True simulation instability |
|
||||||
|
|
||||||
|
**Landscape Classifications**:
|
||||||
|
- `smooth_unimodal`: Single smooth bowl
|
||||||
|
- `smooth_multimodal`: Multiple smooth regions
|
||||||
|
- `rugged_unimodal`: Single rugged region
|
||||||
|
- `rugged_multimodal`: Multiple rugged regions
|
||||||
|
- `noisy`: High noise level
|
||||||
|
|
||||||
|
### 3. Strategy Selector (`strategy_selector.py`)
|
||||||
|
|
||||||
|
**Algorithm Characteristics**:
|
||||||
|
|
||||||
|
**GP-BO (Gaussian Process Bayesian Optimization)**:
|
||||||
|
- Best for: Smooth, expensive functions (like FEA)
|
||||||
|
- Explicit surrogate model with uncertainty quantification
|
||||||
|
- Acquisition function balances exploration/exploitation
|
||||||
|
|
||||||
|
**CMA-ES (Covariance Matrix Adaptation Evolution Strategy)**:
|
||||||
|
- Best for: Smooth unimodal problems
|
||||||
|
- Fast convergence to local optimum
|
||||||
|
- Adapts search distribution to landscape
|
||||||
|
|
||||||
|
**TPE (Tree-structured Parzen Estimator)**:
|
||||||
|
- Best for: Multimodal, rugged, or noisy problems
|
||||||
|
- Robust to noise and discontinuities
|
||||||
|
- Good global exploration
|
||||||
|
|
||||||
|
### 4. Intelligent Optimizer (`intelligent_optimizer.py`)
|
||||||
|
|
||||||
|
**Workflow**:
|
||||||
|
1. Create characterization study (Random/Sobol sampler)
|
||||||
|
2. Run adaptive characterization with stopping criterion
|
||||||
|
3. Analyze final landscape
|
||||||
|
4. Select optimal strategy
|
||||||
|
5. Create optimization study with recommended sampler
|
||||||
|
6. Warm-start from best characterization point
|
||||||
|
7. Run optimization
|
||||||
|
8. Generate intelligence report
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add to `optimization_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"intelligent_optimization": {
|
||||||
|
"enabled": true,
|
||||||
|
"characterization": {
|
||||||
|
"min_trials": 10,
|
||||||
|
"max_trials": 30,
|
||||||
|
"confidence_threshold": 0.85,
|
||||||
|
"check_interval": 5
|
||||||
|
},
|
||||||
|
"landscape_analysis": {
|
||||||
|
"min_trials_for_analysis": 10
|
||||||
|
},
|
||||||
|
"strategy_selection": {
|
||||||
|
"allow_cmaes": true,
|
||||||
|
"allow_gpbo": true,
|
||||||
|
"allow_tpe": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trials": {
|
||||||
|
"n_trials": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
from optimization_engine.intelligent_optimizer import IntelligentOptimizer
|
||||||
|
|
||||||
|
# Create optimizer
|
||||||
|
optimizer = IntelligentOptimizer(
|
||||||
|
study_name="my_optimization",
|
||||||
|
study_dir=Path("studies/my_study/2_results"),
|
||||||
|
config=optimization_config,
|
||||||
|
verbose=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define design variables
|
||||||
|
design_vars = {
|
||||||
|
'parameter1': (lower_bound, upper_bound),
|
||||||
|
'parameter2': (lower_bound, upper_bound)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run Protocol 10
|
||||||
|
results = optimizer.optimize(
|
||||||
|
objective_function=my_objective,
|
||||||
|
design_variables=design_vars,
|
||||||
|
n_trials=50,
|
||||||
|
target_value=target,
|
||||||
|
tolerance=0.1
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Benefits
|
||||||
|
|
||||||
|
**Efficiency**:
|
||||||
|
- **Simple problems**: Early stop at ~10-15 trials (33% reduction)
|
||||||
|
- **Complex problems**: Extended characterization at ~20-30 trials
|
||||||
|
- **Right algorithm**: Uses optimal strategy for landscape type
|
||||||
|
|
||||||
|
**Example Performance** (Circular Plate Frequency Tuning):
|
||||||
|
- TPE alone: ~95 trials to target
|
||||||
|
- Random search: ~150+ trials
|
||||||
|
- **Protocol 10**: ~56 trials (**41% reduction**)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intelligence Reports
|
||||||
|
|
||||||
|
Protocol 10 generates three tracking files:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `characterization_progress.json` | Metric evolution, confidence progression, stopping decision |
|
||||||
|
| `intelligence_report.json` | Final landscape classification, parameter correlations, strategy recommendation |
|
||||||
|
| `strategy_transitions.json` | Phase transitions, algorithm switches, performance metrics |
|
||||||
|
|
||||||
|
**Location**: `studies/{study_name}/2_results/intelligent_optimizer/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| Characterization takes too long | Complex landscape | Increase `max_trials` or accept longer characterization |
|
||||||
|
| Wrong algorithm selected | Insufficient exploration | Lower `confidence_threshold` or increase `min_trials` |
|
||||||
|
| Poor convergence | Mismatch between landscape and algorithm | Review `intelligence_report.json`, consider manual override |
|
||||||
|
| "No characterization data" | Study not using Protocol 10 | Enable `intelligent_optimization.enabled: true` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Depends On**: None
|
||||||
|
- **Used By**: [OP_01_CREATE_STUDY](../operations/OP_01_CREATE_STUDY.md), [OP_02_RUN_OPTIMIZATION](../operations/OP_02_RUN_OPTIMIZATION.md)
|
||||||
|
- **Integrates With**: [SYS_13_DASHBOARD_TRACKING](./SYS_13_DASHBOARD_TRACKING.md)
|
||||||
|
- **See Also**: [SYS_11_MULTI_OBJECTIVE](./SYS_11_MULTI_OBJECTIVE.md) for multi-objective optimization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Files
|
||||||
|
|
||||||
|
- `optimization_engine/intelligent_optimizer.py` - Main orchestrator
|
||||||
|
- `optimization_engine/adaptive_characterization.py` - Stopping criterion
|
||||||
|
- `optimization_engine/landscape_analyzer.py` - Landscape metrics
|
||||||
|
- `optimization_engine/strategy_selector.py` - Algorithm recommendation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 2.1 | 2025-11-20 | Fixed strategy selector timing, multimodality detection, added simulation validation |
|
||||||
|
| 2.0 | 2025-11-20 | Added adaptive characterization, two-study architecture |
|
||||||
|
| 1.0 | 2025-11-19 | Initial implementation |
|
||||||
|
|
||||||
|
### Version 2.1 Bug Fixes Detail
|
||||||
|
|
||||||
|
**Fix #1: Strategy Selector - Use Characterization Trial Count**
|
||||||
|
|
||||||
|
*Problem*: Strategy selector used total trial count (including pruned) instead of characterization trial count, causing wrong algorithm selection after characterization.
|
||||||
|
|
||||||
|
*Solution* (`strategy_selector.py`): Use `char_trials = landscape.get('total_trials', trials_completed)` for decisions.
|
||||||
|
|
||||||
|
**Fix #2: Improved Multimodality Detection**
|
||||||
|
|
||||||
|
*Problem*: False multimodality detected on smooth continuous surfaces (2 modes detected when problem was unimodal).
|
||||||
|
|
||||||
|
*Solution* (`landscape_analyzer.py`): Added heuristic - if only 2 modes with smoothness > 0.6 and noise < 0.2, reclassify as unimodal (smooth continuous manifold).
|
||||||
|
|
||||||
|
**Fix #3: Simulation Validation**
|
||||||
|
|
||||||
|
*Problem*: 20% pruning rate due to extreme parameters causing mesh/solver failures.
|
||||||
|
|
||||||
|
*Solution*: Created `simulation_validator.py` with:
|
||||||
|
- Hard limits (reject invalid parameters)
|
||||||
|
- Soft limits (warn about risky parameters)
|
||||||
|
- Aspect ratio checks
|
||||||
|
- Model-specific validation rules
|
||||||
|
|
||||||
|
*Impact*: Reduced pruning rate from 20% to ~5%.
|
||||||
338
docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md
Normal file
338
docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
# SYS_11: Multi-Objective Support
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Multi-Objective Optimization Support
|
||||||
|
LAYER: System
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active (MANDATORY)
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: user
|
||||||
|
LOAD_WITH: []
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**ALL** optimization engines in Atomizer **MUST** support both single-objective and multi-objective optimization without requiring code changes. This protocol ensures system robustness and prevents runtime failures when handling Pareto optimization.
|
||||||
|
|
||||||
|
**Key Requirement**: Code must work with both `study.best_trial` (single) and `study.best_trials` (multi) APIs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| 2+ objectives defined in config | Use NSGA-II sampler |
|
||||||
|
| "pareto", "multi-objective" mentioned | Load this protocol |
|
||||||
|
| "tradeoff", "competing goals" | Suggest multi-objective approach |
|
||||||
|
| "minimize X AND maximize Y" | Configure as multi-objective |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Single vs. Multi-Objective API**:
|
||||||
|
|
||||||
|
| Operation | Single-Objective | Multi-Objective |
|
||||||
|
|-----------|-----------------|-----------------|
|
||||||
|
| Best trial | `study.best_trial` | `study.best_trials[0]` |
|
||||||
|
| Best params | `study.best_params` | `trial.params` |
|
||||||
|
| Best value | `study.best_value` | `trial.values` (tuple) |
|
||||||
|
| Direction | `direction='minimize'` | `directions=['minimize', 'maximize']` |
|
||||||
|
| Sampler | TPE, CMA-ES, GP | NSGA-II (mandatory) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Problem This Solves
|
||||||
|
|
||||||
|
Previously, optimization components only supported single-objective. When used with multi-objective studies:
|
||||||
|
|
||||||
|
1. Trials run successfully
|
||||||
|
2. Trials saved to database
|
||||||
|
3. **CRASH** when compiling results
|
||||||
|
- `study.best_trial` raises RuntimeError
|
||||||
|
- No tracking files generated
|
||||||
|
- Silent failures
|
||||||
|
|
||||||
|
**Root Cause**: Optuna has different APIs:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Single-Objective (works)
|
||||||
|
study.best_trial # Returns Trial object
|
||||||
|
study.best_params # Returns dict
|
||||||
|
study.best_value # Returns float
|
||||||
|
|
||||||
|
# Multi-Objective (RAISES RuntimeError)
|
||||||
|
study.best_trial # ❌ RuntimeError
|
||||||
|
study.best_params # ❌ RuntimeError
|
||||||
|
study.best_value # ❌ RuntimeError
|
||||||
|
study.best_trials # ✓ Returns LIST of Pareto-optimal trials
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution Pattern
|
||||||
|
|
||||||
|
### 1. Always Check Study Type
|
||||||
|
|
||||||
|
```python
|
||||||
|
is_multi_objective = len(study.directions) > 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Use Conditional Access
|
||||||
|
|
||||||
|
```python
|
||||||
|
if is_multi_objective:
|
||||||
|
best_trials = study.best_trials
|
||||||
|
if best_trials:
|
||||||
|
# Select representative trial (e.g., first Pareto solution)
|
||||||
|
representative_trial = best_trials[0]
|
||||||
|
best_params = representative_trial.params
|
||||||
|
best_value = representative_trial.values # Tuple
|
||||||
|
best_trial_num = representative_trial.number
|
||||||
|
else:
|
||||||
|
best_params = {}
|
||||||
|
best_value = None
|
||||||
|
best_trial_num = None
|
||||||
|
else:
|
||||||
|
# Single-objective: safe to use standard API
|
||||||
|
best_params = study.best_params
|
||||||
|
best_value = study.best_value
|
||||||
|
best_trial_num = study.best_trial.number
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Return Rich Metadata
|
||||||
|
|
||||||
|
Always include in results:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'best_params': best_params,
|
||||||
|
'best_value': best_value, # float or tuple
|
||||||
|
'best_trial': best_trial_num,
|
||||||
|
'is_multi_objective': is_multi_objective,
|
||||||
|
'pareto_front_size': len(study.best_trials) if is_multi_objective else 1,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
When creating or modifying any optimization component:
|
||||||
|
|
||||||
|
- [ ] **Study Creation**: Support `directions` parameter
|
||||||
|
```python
|
||||||
|
if len(objectives) > 1:
|
||||||
|
directions = [obj['type'] for obj in objectives] # ['minimize', 'maximize']
|
||||||
|
study = optuna.create_study(directions=directions, ...)
|
||||||
|
else:
|
||||||
|
study = optuna.create_study(direction='minimize', ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Result Compilation**: Check `len(study.directions) > 1`
|
||||||
|
- [ ] **Best Trial Access**: Use conditional logic
|
||||||
|
- [ ] **Logging**: Print Pareto front size for multi-objective
|
||||||
|
- [ ] **Reports**: Handle tuple objectives in visualization
|
||||||
|
- [ ] **Testing**: Test with BOTH single and multi-objective cases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
**Multi-Objective Config Example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"objectives": [
|
||||||
|
{
|
||||||
|
"name": "stiffness",
|
||||||
|
"type": "maximize",
|
||||||
|
"description": "Structural stiffness (N/mm)",
|
||||||
|
"unit": "N/mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mass",
|
||||||
|
"type": "minimize",
|
||||||
|
"description": "Total mass (kg)",
|
||||||
|
"unit": "kg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization_settings": {
|
||||||
|
"sampler": "NSGAIISampler",
|
||||||
|
"n_trials": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Objective Function Return Format**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Single-objective: return float
|
||||||
|
def objective_single(trial):
|
||||||
|
# ... compute ...
|
||||||
|
return objective_value # float
|
||||||
|
|
||||||
|
# Multi-objective: return tuple
|
||||||
|
def objective_multi(trial):
|
||||||
|
# ... compute ...
|
||||||
|
return (stiffness, mass) # tuple of floats
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Semantic Directions
|
||||||
|
|
||||||
|
Use semantic direction values - no negative tricks:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✅ CORRECT: Semantic directions
|
||||||
|
objectives = [
|
||||||
|
{"name": "stiffness", "type": "maximize"},
|
||||||
|
{"name": "mass", "type": "minimize"}
|
||||||
|
]
|
||||||
|
# Return: (stiffness, mass) - both positive values
|
||||||
|
|
||||||
|
# ❌ WRONG: Negative trick
|
||||||
|
def objective(trial):
|
||||||
|
return (-stiffness, mass) # Don't negate to fake maximize
|
||||||
|
```
|
||||||
|
|
||||||
|
Optuna handles directions correctly when you specify `directions=['maximize', 'minimize']`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Protocol
|
||||||
|
|
||||||
|
Before marking any optimization component complete:
|
||||||
|
|
||||||
|
### Test 1: Single-Objective
|
||||||
|
```python
|
||||||
|
# Config with 1 objective
|
||||||
|
directions = None # or ['minimize']
|
||||||
|
# Run optimization
|
||||||
|
# Verify: completes without errors
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Multi-Objective
|
||||||
|
```python
|
||||||
|
# Config with 2+ objectives
|
||||||
|
directions = ['minimize', 'minimize']
|
||||||
|
# Run optimization
|
||||||
|
# Verify: completes without errors
|
||||||
|
# Verify: ALL tracking files generated
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: Verify Outputs
|
||||||
|
- `2_results/study.db` exists
|
||||||
|
- `2_results/intelligent_optimizer/` has tracking files
|
||||||
|
- `2_results/optimization_summary.json` exists
|
||||||
|
- No RuntimeError in logs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NSGA-II Configuration
|
||||||
|
|
||||||
|
For multi-objective optimization, use NSGA-II:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import optuna
|
||||||
|
from optuna.samplers import NSGAIISampler
|
||||||
|
|
||||||
|
sampler = NSGAIISampler(
|
||||||
|
population_size=50, # Pareto front population
|
||||||
|
mutation_prob=None, # Auto-computed
|
||||||
|
crossover_prob=0.9, # Recombination rate
|
||||||
|
swapping_prob=0.5, # Gene swapping probability
|
||||||
|
seed=42 # Reproducibility
|
||||||
|
)
|
||||||
|
|
||||||
|
study = optuna.create_study(
|
||||||
|
directions=['maximize', 'minimize'],
|
||||||
|
sampler=sampler,
|
||||||
|
study_name="multi_objective_study",
|
||||||
|
storage="sqlite:///study.db"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pareto Front Handling
|
||||||
|
|
||||||
|
### Accessing Pareto Solutions
|
||||||
|
|
||||||
|
```python
|
||||||
|
if is_multi_objective:
|
||||||
|
pareto_trials = study.best_trials
|
||||||
|
print(f"Found {len(pareto_trials)} Pareto-optimal solutions")
|
||||||
|
|
||||||
|
for trial in pareto_trials:
|
||||||
|
print(f"Trial {trial.number}: {trial.values}")
|
||||||
|
print(f" Params: {trial.params}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Selecting Representative Solution
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Option 1: First Pareto solution
|
||||||
|
representative = study.best_trials[0]
|
||||||
|
|
||||||
|
# Option 2: Weighted selection
|
||||||
|
def weighted_selection(trials, weights):
|
||||||
|
best_score = float('inf')
|
||||||
|
best_trial = None
|
||||||
|
for trial in trials:
|
||||||
|
score = sum(w * v for w, v in zip(weights, trial.values))
|
||||||
|
if score < best_score:
|
||||||
|
best_score = score
|
||||||
|
best_trial = trial
|
||||||
|
return best_trial
|
||||||
|
|
||||||
|
# Option 3: Knee point (maximum distance from ideal line)
|
||||||
|
# Requires more complex computation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| RuntimeError on `best_trial` | Multi-objective study using single API | Use conditional check pattern |
|
||||||
|
| Empty Pareto front | No feasible solutions | Check constraints, relax if needed |
|
||||||
|
| Only 1 Pareto solution | Objectives not conflicting | Verify objectives are truly competing |
|
||||||
|
| NSGA-II with single objective | Wrong config | Use TPE/CMA-ES for single-objective |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Depends On**: None (mandatory for all)
|
||||||
|
- **Used By**: All optimization components
|
||||||
|
- **Integrates With**:
|
||||||
|
- [SYS_10_IMSO](./SYS_10_IMSO.md) (selects NSGA-II for multi-objective)
|
||||||
|
- [SYS_13_DASHBOARD_TRACKING](./SYS_13_DASHBOARD_TRACKING.md) (Pareto visualization)
|
||||||
|
- **See Also**: [OP_04_ANALYZE_RESULTS](../operations/OP_04_ANALYZE_RESULTS.md) for Pareto analysis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Files
|
||||||
|
|
||||||
|
Files that implement this protocol:
|
||||||
|
- `optimization_engine/intelligent_optimizer.py` - `_compile_results()` method
|
||||||
|
- `optimization_engine/study_continuation.py` - Result handling
|
||||||
|
- `optimization_engine/hybrid_study_creator.py` - Study creation
|
||||||
|
|
||||||
|
Files requiring this protocol:
|
||||||
|
- [ ] `optimization_engine/study_continuation.py`
|
||||||
|
- [ ] `optimization_engine/hybrid_study_creator.py`
|
||||||
|
- [ ] `optimization_engine/intelligent_setup.py`
|
||||||
|
- [ ] `optimization_engine/llm_optimization_runner.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-11-20 | Initial release, mandatory for all engines |
|
||||||
435
docs/protocols/system/SYS_13_DASHBOARD_TRACKING.md
Normal file
435
docs/protocols/system/SYS_13_DASHBOARD_TRACKING.md
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
# SYS_13: Real-Time Dashboard Tracking
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Real-Time Dashboard Tracking
|
||||||
|
LAYER: System
|
||||||
|
VERSION: 1.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-05
|
||||||
|
PRIVILEGE: user
|
||||||
|
LOAD_WITH: [SYS_10_IMSO, SYS_11_MULTI_OBJECTIVE]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Protocol 13 implements a comprehensive real-time web dashboard for monitoring optimization studies. It provides live visualization of optimizer state, Pareto fronts, parallel coordinates, and trial history with automatic updates every trial.
|
||||||
|
|
||||||
|
**Key Feature**: Every trial completion writes state to JSON, enabling live browser updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| "dashboard", "visualization" mentioned | Load this protocol |
|
||||||
|
| "real-time", "monitoring" requested | Enable dashboard tracking |
|
||||||
|
| Multi-objective study | Dashboard shows Pareto front |
|
||||||
|
| Want to see progress visually | Point to `localhost:3000` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Dashboard URLs**:
|
||||||
|
| Service | URL | Purpose |
|
||||||
|
|---------|-----|---------|
|
||||||
|
| Frontend | `http://localhost:3000` | Main dashboard |
|
||||||
|
| Backend API | `http://localhost:8000` | REST API |
|
||||||
|
| Optuna Dashboard | `http://localhost:8080` | Alternative viewer |
|
||||||
|
|
||||||
|
**Start Commands**:
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd atomizer-dashboard/backend
|
||||||
|
python -m uvicorn api.main:app --reload --port 8000
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd atomizer-dashboard/frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Trial Completion (Optuna)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Realtime Callback (optimization_engine/realtime_tracking.py)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Write optimizer_state.json
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Backend API /optimizer-state endpoint
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Frontend Components (2s polling)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
User sees live updates in browser
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend Components
|
||||||
|
|
||||||
|
### 1. Real-Time Tracking System (`realtime_tracking.py`)
|
||||||
|
|
||||||
|
**Purpose**: Write JSON state files after every trial completion.
|
||||||
|
|
||||||
|
**Integration** (in `intelligent_optimizer.py`):
|
||||||
|
```python
|
||||||
|
from optimization_engine.realtime_tracking import create_realtime_callback
|
||||||
|
|
||||||
|
# Create callback
|
||||||
|
callback = create_realtime_callback(
|
||||||
|
tracking_dir=results_dir / "intelligent_optimizer",
|
||||||
|
optimizer_ref=self,
|
||||||
|
verbose=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register with Optuna
|
||||||
|
study.optimize(objective, n_trials=n_trials, callbacks=[callback])
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data Structure** (`optimizer_state.json`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2025-11-21T15:27:28.828930",
|
||||||
|
"trial_number": 29,
|
||||||
|
"total_trials": 50,
|
||||||
|
"current_phase": "adaptive_optimization",
|
||||||
|
"current_strategy": "GP_UCB",
|
||||||
|
"is_multi_objective": true,
|
||||||
|
"study_directions": ["maximize", "minimize"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. REST API Endpoints
|
||||||
|
|
||||||
|
**Base**: `/api/optimization/studies/{study_id}/`
|
||||||
|
|
||||||
|
| Endpoint | Method | Returns |
|
||||||
|
|----------|--------|---------|
|
||||||
|
| `/metadata` | GET | Objectives, design vars, constraints with units |
|
||||||
|
| `/optimizer-state` | GET | Current phase, strategy, progress |
|
||||||
|
| `/pareto-front` | GET | Pareto-optimal solutions (multi-objective) |
|
||||||
|
| `/history` | GET | All trial history |
|
||||||
|
| `/` | GET | List all studies |
|
||||||
|
|
||||||
|
**Unit Inference**:
|
||||||
|
```python
|
||||||
|
def _infer_objective_unit(objective: Dict) -> str:
|
||||||
|
name = objective.get("name", "").lower()
|
||||||
|
desc = objective.get("description", "").lower()
|
||||||
|
|
||||||
|
if "frequency" in name or "hz" in desc:
|
||||||
|
return "Hz"
|
||||||
|
elif "stiffness" in name or "n/mm" in desc:
|
||||||
|
return "N/mm"
|
||||||
|
elif "mass" in name or "kg" in desc:
|
||||||
|
return "kg"
|
||||||
|
# ... more patterns
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Components
|
||||||
|
|
||||||
|
### 1. OptimizerPanel (`components/OptimizerPanel.tsx`)
|
||||||
|
|
||||||
|
**Displays**:
|
||||||
|
- Current phase (Characterization, Exploration, Exploitation, Adaptive)
|
||||||
|
- Current strategy (TPE, GP, NSGA-II, etc.)
|
||||||
|
- Progress bar with trial count
|
||||||
|
- Multi-objective indicator
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Intelligent Optimizer Status │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ Phase: [Adaptive Optimization] │
|
||||||
|
│ Strategy: [GP_UCB] │
|
||||||
|
│ Progress: [████████░░] 29/50 │
|
||||||
|
│ Multi-Objective: ✓ │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. ParetoPlot (`components/ParetoPlot.tsx`)
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Scatter plot of Pareto-optimal solutions
|
||||||
|
- Pareto front line connecting optimal points
|
||||||
|
- **3 Normalization Modes**:
|
||||||
|
- **Raw**: Original engineering values
|
||||||
|
- **Min-Max**: Scales to [0, 1]
|
||||||
|
- **Z-Score**: Standardizes to mean=0, std=1
|
||||||
|
- Tooltip shows raw values regardless of normalization
|
||||||
|
- Color-coded: green=feasible, red=infeasible
|
||||||
|
|
||||||
|
### 3. ParallelCoordinatesPlot (`components/ParallelCoordinatesPlot.tsx`)
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- High-dimensional visualization (objectives + design variables)
|
||||||
|
- Interactive trial selection
|
||||||
|
- Normalized [0, 1] axes
|
||||||
|
- Color coding: green (feasible), red (infeasible), yellow (selected)
|
||||||
|
|
||||||
|
```
|
||||||
|
Stiffness Mass support_angle tip_thickness
|
||||||
|
│ │ │ │
|
||||||
|
│ ╱─────╲ ╱ │
|
||||||
|
│ ╱ ╲─────────╱ │
|
||||||
|
│ ╱ ╲ │
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Dashboard Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ Study Selection │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ Metrics Grid (Best, Avg, Trials, Pruned) │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ [OptimizerPanel] [ParetoPlot] │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ [ParallelCoordinatesPlot - Full Width] │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ [Convergence] [Parameter Space] │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ [Recent Trials Table] │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
**In `optimization_config.json`**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dashboard_settings": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 8000,
|
||||||
|
"realtime_updates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Study Requirements**:
|
||||||
|
- Must use Protocol 10 (IntelligentOptimizer) for optimizer state
|
||||||
|
- Must have `optimization_config.json` with objectives and design_variables
|
||||||
|
- Real-time tracking enabled automatically with Protocol 10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage Workflow
|
||||||
|
|
||||||
|
### 1. Start Dashboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Backend
|
||||||
|
cd atomizer-dashboard/backend
|
||||||
|
python -m uvicorn api.main:app --reload --port 8000
|
||||||
|
|
||||||
|
# Terminal 2: Frontend
|
||||||
|
cd atomizer-dashboard/frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start Optimization
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd studies/my_study
|
||||||
|
conda activate atomizer
|
||||||
|
python run_optimization.py --n-trials 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. View Dashboard
|
||||||
|
|
||||||
|
- Open browser to `http://localhost:3000`
|
||||||
|
- Select study from dropdown
|
||||||
|
- Watch real-time updates every trial
|
||||||
|
|
||||||
|
### 4. Interact with Plots
|
||||||
|
|
||||||
|
- Toggle normalization on Pareto plot
|
||||||
|
- Click lines in parallel coordinates to select trials
|
||||||
|
- Hover for detailed trial information
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Backend endpoint latency | ~10ms |
|
||||||
|
| Frontend polling interval | 2 seconds |
|
||||||
|
| Real-time write overhead | <5ms per trial |
|
||||||
|
| Dashboard initial load | <500ms |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Other Protocols
|
||||||
|
|
||||||
|
### Protocol 10 Integration
|
||||||
|
- Real-time callback integrated into `IntelligentOptimizer.optimize()`
|
||||||
|
- Tracks phase transitions (characterization → adaptive optimization)
|
||||||
|
- Reports strategy changes
|
||||||
|
|
||||||
|
### Protocol 11 Integration
|
||||||
|
- Pareto front endpoint checks `len(study.directions) > 1`
|
||||||
|
- Dashboard conditionally renders Pareto plots
|
||||||
|
- Uses Optuna's `study.best_trials` for Pareto front
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| "No Pareto front data yet" | Single-objective or no trials | Wait for trials, check objectives |
|
||||||
|
| OptimizerPanel shows "Not available" | Not using Protocol 10 | Enable IntelligentOptimizer |
|
||||||
|
| Units not showing | Missing unit in config | Add `unit` field or use pattern in description |
|
||||||
|
| Dashboard not updating | Backend not running | Start backend with uvicorn |
|
||||||
|
| CORS errors | Backend/frontend mismatch | Check ports, restart both |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Depends On**: [SYS_10_IMSO](./SYS_10_IMSO.md), [SYS_11_MULTI_OBJECTIVE](./SYS_11_MULTI_OBJECTIVE.md)
|
||||||
|
- **Used By**: [OP_03_MONITOR_PROGRESS](../operations/OP_03_MONITOR_PROGRESS.md)
|
||||||
|
- **See Also**: Optuna Dashboard for alternative visualization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Files
|
||||||
|
|
||||||
|
**Backend**:
|
||||||
|
- `atomizer-dashboard/backend/api/main.py` - FastAPI app
|
||||||
|
- `atomizer-dashboard/backend/api/routes/optimization.py` - Endpoints
|
||||||
|
- `optimization_engine/realtime_tracking.py` - Callback system
|
||||||
|
|
||||||
|
**Frontend**:
|
||||||
|
- `atomizer-dashboard/frontend/src/pages/Dashboard.tsx` - Main page
|
||||||
|
- `atomizer-dashboard/frontend/src/components/OptimizerPanel.tsx`
|
||||||
|
- `atomizer-dashboard/frontend/src/components/ParetoPlot.tsx`
|
||||||
|
- `atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Backend API Example (FastAPI)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@router.get("/studies/{study_id}/pareto-front")
|
||||||
|
async def get_pareto_front(study_id: str):
|
||||||
|
"""Get Pareto-optimal solutions for multi-objective studies."""
|
||||||
|
study = optuna.load_study(study_name=study_id, storage=storage)
|
||||||
|
|
||||||
|
if len(study.directions) == 1:
|
||||||
|
return {"is_multi_objective": False}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"is_multi_objective": True,
|
||||||
|
"pareto_front": [
|
||||||
|
{
|
||||||
|
"trial_number": t.number,
|
||||||
|
"values": t.values,
|
||||||
|
"params": t.params,
|
||||||
|
"user_attrs": dict(t.user_attrs)
|
||||||
|
}
|
||||||
|
for t in study.best_trials
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend OptimizerPanel (React/TypeScript)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function OptimizerPanel({ studyId }: { studyId: string }) {
|
||||||
|
const [state, setState] = useState<OptimizerState | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchState = async () => {
|
||||||
|
const res = await fetch(`/api/optimization/studies/${studyId}/optimizer-state`);
|
||||||
|
setState(await res.json());
|
||||||
|
};
|
||||||
|
fetchState();
|
||||||
|
const interval = setInterval(fetchState, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [studyId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Optimizer Status">
|
||||||
|
<div>Phase: {state?.current_phase}</div>
|
||||||
|
<div>Strategy: {state?.current_strategy}</div>
|
||||||
|
<ProgressBar value={state?.trial_number} max={state?.total_trials} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Callback Integration
|
||||||
|
|
||||||
|
**CRITICAL**: Every `study.optimize()` call must include the realtime callback:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In IntelligentOptimizer
|
||||||
|
self.realtime_callback = create_realtime_callback(
|
||||||
|
tracking_dir=self.tracking_dir,
|
||||||
|
optimizer_ref=self,
|
||||||
|
verbose=self.verbose
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register with ALL optimize calls
|
||||||
|
self.study.optimize(
|
||||||
|
objective_function,
|
||||||
|
n_trials=check_interval,
|
||||||
|
callbacks=[self.realtime_callback] # Required for real-time updates
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chart Library Options
|
||||||
|
|
||||||
|
The dashboard supports two chart libraries:
|
||||||
|
|
||||||
|
| Feature | Recharts | Plotly |
|
||||||
|
|---------|----------|--------|
|
||||||
|
| Load Speed | Fast | Slower (lazy loaded) |
|
||||||
|
| Interactivity | Basic | Advanced |
|
||||||
|
| Export | Screenshot | PNG/SVG native |
|
||||||
|
| 3D Support | No | Yes |
|
||||||
|
| Real-time Updates | Better | Good |
|
||||||
|
|
||||||
|
**Recommendation**: Use Recharts during active optimization, Plotly for post-analysis.
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Both backend and frontend
|
||||||
|
python start_dashboard.py
|
||||||
|
|
||||||
|
# Or manually:
|
||||||
|
cd atomizer-dashboard/backend && python -m uvicorn main:app --port 8000
|
||||||
|
cd atomizer-dashboard/frontend && npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Access at: `http://localhost:3003`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.2 | 2025-12-05 | Added chart library options |
|
||||||
|
| 1.1 | 2025-12-05 | Added implementation code snippets |
|
||||||
|
| 1.0 | 2025-11-21 | Initial release with real-time tracking |
|
||||||
564
docs/protocols/system/SYS_14_NEURAL_ACCELERATION.md
Normal file
564
docs/protocols/system/SYS_14_NEURAL_ACCELERATION.md
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
# SYS_14: Neural Network Acceleration
|
||||||
|
|
||||||
|
<!--
|
||||||
|
PROTOCOL: Neural Network Surrogate Acceleration
|
||||||
|
LAYER: System
|
||||||
|
VERSION: 2.0
|
||||||
|
STATUS: Active
|
||||||
|
LAST_UPDATED: 2025-12-06
|
||||||
|
PRIVILEGE: user
|
||||||
|
LOAD_WITH: [SYS_10_IMSO, SYS_11_MULTI_OBJECTIVE]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Atomizer provides **neural network surrogate acceleration** enabling 100-1000x faster optimization by replacing expensive FEA evaluations with instant neural predictions.
|
||||||
|
|
||||||
|
**Two approaches available**:
|
||||||
|
1. **MLP Surrogate** (Simple, integrated) - 4-layer MLP trained on FEA data, runs within study
|
||||||
|
2. **GNN Field Predictor** (Advanced) - Graph neural network for full field predictions
|
||||||
|
|
||||||
|
**Key Innovation**: Train once on FEA data, then explore 5,000-50,000+ designs in the time it takes to run 50 FEA trials.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
| Trigger | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| >50 trials needed | Consider neural acceleration |
|
||||||
|
| "neural", "surrogate", "NN" mentioned | Load this protocol |
|
||||||
|
| "fast", "acceleration", "speed" needed | Suggest neural acceleration |
|
||||||
|
| Training data available | Enable surrogate |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Performance Comparison**:
|
||||||
|
|
||||||
|
| Metric | Traditional FEA | Neural Network | Improvement |
|
||||||
|
|--------|-----------------|----------------|-------------|
|
||||||
|
| Time per evaluation | 10-30 minutes | 4.5 milliseconds | **2,000-500,000x** |
|
||||||
|
| Trials per hour | 2-6 | 800,000+ | **1000x** |
|
||||||
|
| Design exploration | ~50 designs | ~50,000 designs | **1000x** |
|
||||||
|
|
||||||
|
**Model Types**:
|
||||||
|
|
||||||
|
| Model | Purpose | Use When |
|
||||||
|
|-------|---------|----------|
|
||||||
|
| **MLP Surrogate** | Direct objective prediction | Simple studies, quick setup |
|
||||||
|
| Field Predictor GNN | Full displacement/stress fields | Need field visualization |
|
||||||
|
| Parametric Predictor GNN | Direct objective prediction | Complex geometry, need accuracy |
|
||||||
|
| Ensemble | Uncertainty quantification | Need confidence bounds |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MLP Surrogate (Recommended for Quick Start)
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
The MLP (Multi-Layer Perceptron) surrogate is a simple but effective neural network that predicts objectives directly from design parameters. It's integrated into the study workflow via `run_nn_optimization.py`.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Input Layer (N design variables)
|
||||||
|
↓
|
||||||
|
Linear(N, 64) + ReLU + BatchNorm + Dropout(0.1)
|
||||||
|
↓
|
||||||
|
Linear(64, 128) + ReLU + BatchNorm + Dropout(0.1)
|
||||||
|
↓
|
||||||
|
Linear(128, 128) + ReLU + BatchNorm + Dropout(0.1)
|
||||||
|
↓
|
||||||
|
Linear(128, 64) + ReLU + BatchNorm + Dropout(0.1)
|
||||||
|
↓
|
||||||
|
Linear(64, M objectives)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**: ~34,000 trainable
|
||||||
|
|
||||||
|
### Workflow Modes
|
||||||
|
|
||||||
|
#### 1. Standard Hybrid Mode (`--all`)
|
||||||
|
|
||||||
|
Run all phases sequentially:
|
||||||
|
```bash
|
||||||
|
python run_nn_optimization.py --all
|
||||||
|
```
|
||||||
|
|
||||||
|
Phases:
|
||||||
|
1. **Export**: Extract training data from existing FEA trials
|
||||||
|
2. **Train**: Train MLP surrogate (300 epochs default)
|
||||||
|
3. **NN-Optimize**: Run 1000 NN trials with NSGA-II
|
||||||
|
4. **Validate**: Validate top 10 candidates with FEA
|
||||||
|
|
||||||
|
#### 2. Hybrid Loop Mode (`--hybrid-loop`)
|
||||||
|
|
||||||
|
Iterative refinement:
|
||||||
|
```bash
|
||||||
|
python run_nn_optimization.py --hybrid-loop --iterations 5 --nn-trials 500
|
||||||
|
```
|
||||||
|
|
||||||
|
Each iteration:
|
||||||
|
1. Train/retrain surrogate from current FEA data
|
||||||
|
2. Run NN optimization
|
||||||
|
3. Validate top candidates with FEA
|
||||||
|
4. Add validated results to training set
|
||||||
|
5. Repeat until convergence (max error < 5%)
|
||||||
|
|
||||||
|
#### 3. Turbo Mode (`--turbo`) ⚡ RECOMMENDED
|
||||||
|
|
||||||
|
Aggressive single-best validation:
|
||||||
|
```bash
|
||||||
|
python run_nn_optimization.py --turbo --nn-trials 5000 --batch-size 100 --retrain-every 10
|
||||||
|
```
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
- Run NN in small batches (100 trials)
|
||||||
|
- Validate ONLY the single best candidate with FEA
|
||||||
|
- Add to training data immediately
|
||||||
|
- Retrain surrogate every N FEA validations
|
||||||
|
- Repeat until total NN budget exhausted
|
||||||
|
|
||||||
|
**Example**: 5,000 NN trials with batch=100 → 50 FEA validations in ~12 minutes
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"neural_acceleration": {
|
||||||
|
"enabled": true,
|
||||||
|
"min_training_points": 50,
|
||||||
|
"auto_train": true,
|
||||||
|
"epochs": 300,
|
||||||
|
"validation_split": 0.2,
|
||||||
|
"nn_trials": 1000,
|
||||||
|
"validate_top_n": 10,
|
||||||
|
"model_file": "surrogate_best.pt",
|
||||||
|
"separate_nn_database": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: `separate_nn_database: true` stores NN trials in `nn_study.db` instead of `study.db` to avoid overloading the dashboard with thousands of NN-only results.
|
||||||
|
|
||||||
|
### Typical Accuracy
|
||||||
|
|
||||||
|
| Objective | Expected Error |
|
||||||
|
|-----------|----------------|
|
||||||
|
| Mass | 1-5% |
|
||||||
|
| Stress | 1-4% |
|
||||||
|
| Stiffness | 5-15% |
|
||||||
|
|
||||||
|
### Output Files
|
||||||
|
|
||||||
|
```
|
||||||
|
2_results/
|
||||||
|
├── study.db # Main FEA + validated results (dashboard)
|
||||||
|
├── nn_study.db # NN-only results (not in dashboard)
|
||||||
|
├── surrogate_best.pt # Trained model weights
|
||||||
|
├── training_data.json # Normalized training data
|
||||||
|
├── nn_optimization_state.json # NN optimization state
|
||||||
|
├── nn_pareto_front.json # NN-predicted Pareto front
|
||||||
|
├── validation_report.json # FEA validation results
|
||||||
|
└── turbo_report.json # Turbo mode results (if used)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GNN Field Predictor (Advanced)
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
| Component | File | Purpose |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| BDF/OP2 Parser | `neural_field_parser.py` | Convert NX files to neural format |
|
||||||
|
| Data Validator | `validate_parsed_data.py` | Physics and quality checks |
|
||||||
|
| Field Predictor | `field_predictor.py` | GNN for full field prediction |
|
||||||
|
| Parametric Predictor | `parametric_predictor.py` | GNN for direct objectives |
|
||||||
|
| Physics Loss | `physics_losses.py` | Physics-informed training |
|
||||||
|
| Neural Surrogate | `neural_surrogate.py` | Integration with Atomizer |
|
||||||
|
| Neural Runner | `runner_with_neural.py` | Optimization with NN acceleration |
|
||||||
|
|
||||||
|
### Workflow Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
Traditional:
|
||||||
|
Design → NX Model → Mesh → Solve (30 min) → Results → Objective
|
||||||
|
|
||||||
|
Neural (after training):
|
||||||
|
Design → Neural Network (4.5 ms) → Results → Objective
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Neural Model Types
|
||||||
|
|
||||||
|
### 1. Field Predictor GNN
|
||||||
|
|
||||||
|
**Use Case**: When you need full field predictions (stress distribution, deformation shape).
|
||||||
|
|
||||||
|
```
|
||||||
|
Input Features (12D per node):
|
||||||
|
├── Node coordinates (x, y, z)
|
||||||
|
├── Material properties (E, nu, rho)
|
||||||
|
├── Boundary conditions (fixed/free per DOF)
|
||||||
|
└── Load information (force magnitude, direction)
|
||||||
|
|
||||||
|
GNN Layers (6 message passing):
|
||||||
|
├── MeshGraphConv (custom for FEA topology)
|
||||||
|
├── Layer normalization
|
||||||
|
├── ReLU activation
|
||||||
|
└── Dropout (0.1)
|
||||||
|
|
||||||
|
Output (per node):
|
||||||
|
├── Displacement (6 DOF: Tx, Ty, Tz, Rx, Ry, Rz)
|
||||||
|
└── Von Mises stress (1 value)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**: ~718,221 trainable
|
||||||
|
|
||||||
|
### 2. Parametric Predictor GNN (Recommended)
|
||||||
|
|
||||||
|
**Use Case**: Direct optimization objective prediction (fastest option).
|
||||||
|
|
||||||
|
```
|
||||||
|
Design Parameters (ND) → Design Encoder (MLP) → GNN Backbone → Scalar Heads
|
||||||
|
|
||||||
|
Output (objectives):
|
||||||
|
├── mass (grams)
|
||||||
|
├── frequency (Hz)
|
||||||
|
├── max_displacement (mm)
|
||||||
|
└── max_stress (MPa)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**: ~500,000 trainable
|
||||||
|
|
||||||
|
### 3. Ensemble Models
|
||||||
|
|
||||||
|
**Use Case**: Uncertainty quantification.
|
||||||
|
|
||||||
|
1. Train 3-5 models with different random seeds
|
||||||
|
2. At inference, run all models
|
||||||
|
3. Use mean for prediction, std for uncertainty
|
||||||
|
4. High uncertainty → trigger FEA validation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Training Pipeline
|
||||||
|
|
||||||
|
### Step 1: Collect Training Data
|
||||||
|
|
||||||
|
Enable export in workflow config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"training_data_export": {
|
||||||
|
"enabled": true,
|
||||||
|
"export_dir": "atomizer_field_training_data/my_study"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Output structure:
|
||||||
|
```
|
||||||
|
atomizer_field_training_data/my_study/
|
||||||
|
├── trial_0001/
|
||||||
|
│ ├── input/model.bdf # Nastran input
|
||||||
|
│ ├── output/model.op2 # Binary results
|
||||||
|
│ └── metadata.json # Design params + objectives
|
||||||
|
├── trial_0002/
|
||||||
|
│ └── ...
|
||||||
|
└── study_summary.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended**: 100-500 FEA samples for good generalization.
|
||||||
|
|
||||||
|
### Step 2: Parse to Neural Format
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd atomizer-field
|
||||||
|
python batch_parser.py ../atomizer_field_training_data/my_study
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates HDF5 + JSON files per trial.
|
||||||
|
|
||||||
|
### Step 3: Train Model
|
||||||
|
|
||||||
|
**Parametric Predictor** (recommended):
|
||||||
|
```bash
|
||||||
|
python train_parametric.py \
|
||||||
|
--train_dir ../training_data/parsed \
|
||||||
|
--val_dir ../validation_data/parsed \
|
||||||
|
--epochs 200 \
|
||||||
|
--hidden_channels 128 \
|
||||||
|
--num_layers 4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field Predictor**:
|
||||||
|
```bash
|
||||||
|
python train.py \
|
||||||
|
--train_dir ../training_data/parsed \
|
||||||
|
--epochs 200 \
|
||||||
|
--model FieldPredictorGNN \
|
||||||
|
--hidden_channels 128 \
|
||||||
|
--num_layers 6 \
|
||||||
|
--physics_loss_weight 0.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Validate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python validate.py --checkpoint runs/my_model/checkpoint_best.pt
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
Validation Results:
|
||||||
|
├── Mean Absolute Error: 2.3% (mass), 1.8% (frequency)
|
||||||
|
├── R² Score: 0.987
|
||||||
|
├── Inference Time: 4.5ms ± 0.8ms
|
||||||
|
└── Physics Violations: 0.2%
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Deploy
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"neural_surrogate": {
|
||||||
|
"enabled": true,
|
||||||
|
"model_checkpoint": "atomizer-field/runs/my_model/checkpoint_best.pt",
|
||||||
|
"confidence_threshold": 0.85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Full Neural Configuration Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"study_name": "bracket_neural_optimization",
|
||||||
|
|
||||||
|
"surrogate_settings": {
|
||||||
|
"enabled": true,
|
||||||
|
"model_type": "parametric_gnn",
|
||||||
|
"model_path": "models/bracket_surrogate.pt",
|
||||||
|
"confidence_threshold": 0.85,
|
||||||
|
"validation_frequency": 10,
|
||||||
|
"fallback_to_fea": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"training_data_export": {
|
||||||
|
"enabled": true,
|
||||||
|
"export_dir": "atomizer_field_training_data/bracket_study",
|
||||||
|
"export_bdf": true,
|
||||||
|
"export_op2": true,
|
||||||
|
"export_fields": ["displacement", "stress"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"neural_optimization": {
|
||||||
|
"initial_fea_trials": 50,
|
||||||
|
"neural_trials": 5000,
|
||||||
|
"retraining_interval": 500,
|
||||||
|
"uncertainty_threshold": 0.15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | false | Enable neural surrogate |
|
||||||
|
| `model_type` | string | "parametric_gnn" | Model architecture |
|
||||||
|
| `model_path` | string | - | Path to trained model |
|
||||||
|
| `confidence_threshold` | float | 0.85 | Min confidence for predictions |
|
||||||
|
| `validation_frequency` | int | 10 | FEA validation every N trials |
|
||||||
|
| `fallback_to_fea` | bool | true | Use FEA when uncertain |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hybrid FEA/Neural Workflow
|
||||||
|
|
||||||
|
### Phase 1: FEA Exploration (50-100 trials)
|
||||||
|
- Run standard FEA optimization
|
||||||
|
- Export training data automatically
|
||||||
|
- Build landscape understanding
|
||||||
|
|
||||||
|
### Phase 2: Neural Training
|
||||||
|
- Parse collected data
|
||||||
|
- Train parametric predictor
|
||||||
|
- Validate accuracy
|
||||||
|
|
||||||
|
### Phase 3: Neural Acceleration (1000s of trials)
|
||||||
|
- Use neural network for rapid exploration
|
||||||
|
- Periodic FEA validation
|
||||||
|
- Retrain if distribution shifts
|
||||||
|
|
||||||
|
### Phase 4: FEA Refinement (10-20 trials)
|
||||||
|
- Validate top candidates with FEA
|
||||||
|
- Ensure results are physically accurate
|
||||||
|
- Generate final Pareto front
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adaptive Iteration Loop
|
||||||
|
|
||||||
|
For complex optimizations, use iterative refinement:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Iteration 1: │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ Initial FEA │ -> │ Train NN │ -> │ NN Search │ │
|
||||||
|
│ │ (50-100) │ │ Surrogate │ │ (1000 trials)│ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ Iteration 2+: ▼ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ Validate Top │ -> │ Retrain NN │ -> │ NN Search │ │
|
||||||
|
│ │ NN with FEA │ │ with new data│ │ (1000 trials)│ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adaptive Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"adaptive_settings": {
|
||||||
|
"enabled": true,
|
||||||
|
"initial_fea_trials": 50,
|
||||||
|
"nn_trials_per_iteration": 1000,
|
||||||
|
"fea_validation_per_iteration": 5,
|
||||||
|
"max_iterations": 10,
|
||||||
|
"convergence_threshold": 0.01,
|
||||||
|
"retrain_epochs": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Convergence Criteria
|
||||||
|
|
||||||
|
Stop when:
|
||||||
|
- No improvement for 2-3 consecutive iterations
|
||||||
|
- Reached FEA budget limit
|
||||||
|
- Objective improvement < 1% threshold
|
||||||
|
|
||||||
|
### Output Files
|
||||||
|
|
||||||
|
```
|
||||||
|
studies/my_study/3_results/
|
||||||
|
├── adaptive_state.json # Current iteration state
|
||||||
|
├── surrogate_model.pt # Trained neural network
|
||||||
|
└── training_history.json # NN training metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loss Functions
|
||||||
|
|
||||||
|
### Data Loss (MSE)
|
||||||
|
Standard prediction error:
|
||||||
|
```python
|
||||||
|
data_loss = MSE(predicted, target)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Physics Loss
|
||||||
|
Enforce physical constraints:
|
||||||
|
```python
|
||||||
|
physics_loss = (
|
||||||
|
equilibrium_loss + # Force balance
|
||||||
|
boundary_loss + # BC satisfaction
|
||||||
|
compatibility_loss # Strain compatibility
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined Training
|
||||||
|
```python
|
||||||
|
total_loss = data_loss + 0.3 * physics_loss
|
||||||
|
```
|
||||||
|
|
||||||
|
Physics loss weight typically 0.1-0.5.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uncertainty Quantification
|
||||||
|
|
||||||
|
### Ensemble Method
|
||||||
|
```python
|
||||||
|
# Run N models
|
||||||
|
predictions = [model_i(x) for model_i in ensemble]
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
mean_prediction = np.mean(predictions)
|
||||||
|
uncertainty = np.std(predictions)
|
||||||
|
|
||||||
|
# Decision
|
||||||
|
if uncertainty > threshold:
|
||||||
|
# Use FEA instead
|
||||||
|
result = run_fea(x)
|
||||||
|
else:
|
||||||
|
result = mean_prediction
|
||||||
|
```
|
||||||
|
|
||||||
|
### Confidence Thresholds
|
||||||
|
|
||||||
|
| Uncertainty | Action |
|
||||||
|
|-------------|--------|
|
||||||
|
| < 5% | Use neural prediction |
|
||||||
|
| 5-15% | Use neural, flag for validation |
|
||||||
|
| > 15% | Fall back to FEA |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Solution |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| High prediction error | Insufficient training data | Collect more FEA samples |
|
||||||
|
| Out-of-distribution warnings | Design outside training range | Retrain with expanded range |
|
||||||
|
| Slow inference | Large mesh | Use parametric predictor instead |
|
||||||
|
| Physics violations | Low physics loss weight | Increase `physics_loss_weight` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- **Depends On**: [SYS_10_IMSO](./SYS_10_IMSO.md) for optimization framework
|
||||||
|
- **Used By**: [OP_02_RUN_OPTIMIZATION](../operations/OP_02_RUN_OPTIMIZATION.md), [OP_05_EXPORT_TRAINING_DATA](../operations/OP_05_EXPORT_TRAINING_DATA.md)
|
||||||
|
- **See Also**: [modules/neural-acceleration.md](../../.claude/skills/modules/neural-acceleration.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Files
|
||||||
|
|
||||||
|
```
|
||||||
|
atomizer-field/
|
||||||
|
├── neural_field_parser.py # BDF/OP2 parsing
|
||||||
|
├── field_predictor.py # Field GNN
|
||||||
|
├── parametric_predictor.py # Parametric GNN
|
||||||
|
├── train.py # Field training
|
||||||
|
├── train_parametric.py # Parametric training
|
||||||
|
├── validate.py # Model validation
|
||||||
|
├── physics_losses.py # Physics-informed loss
|
||||||
|
└── batch_parser.py # Batch data conversion
|
||||||
|
|
||||||
|
optimization_engine/
|
||||||
|
├── neural_surrogate.py # Atomizer integration
|
||||||
|
└── runner_with_neural.py # Neural runner
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 2.0 | 2025-12-06 | Added MLP Surrogate with Turbo Mode |
|
||||||
|
| 1.0 | 2025-12-05 | Initial consolidation from neural docs |
|
||||||
344
optimization_engine/hooks/README.md
Normal file
344
optimization_engine/hooks/README.md
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
# Atomizer NX Open Hooks
|
||||||
|
|
||||||
|
Direct Python hooks for NX CAD/CAE operations via NX Open API.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This module provides a clean Python API for manipulating NX parts programmatically. Each hook executes NX journals via `run_journal.exe` and returns structured JSON results.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
hooks/
|
||||||
|
├── __init__.py # Main entry point
|
||||||
|
├── README.md # This file
|
||||||
|
├── test_hooks.py # Test script
|
||||||
|
└── nx_cad/ # CAD manipulation hooks
|
||||||
|
├── __init__.py
|
||||||
|
├── part_manager.py # Open/Close/Save parts
|
||||||
|
├── expression_manager.py # Get/Set expressions
|
||||||
|
├── geometry_query.py # Mass properties, bodies
|
||||||
|
└── feature_manager.py # Suppress/Unsuppress features
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **NX Installation**: Siemens NX 2506 or compatible version
|
||||||
|
- **Environment Variable**: `NX_BIN_PATH` (defaults to `C:\Program Files\Siemens\NX2506\NXBIN`)
|
||||||
|
- **Python**: 3.8+ with `atomizer` conda environment
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```python
|
||||||
|
from optimization_engine.hooks.nx_cad import (
|
||||||
|
part_manager,
|
||||||
|
expression_manager,
|
||||||
|
geometry_query,
|
||||||
|
feature_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Path to your NX part
|
||||||
|
part_path = "C:/path/to/model.prt"
|
||||||
|
|
||||||
|
# Get all expressions
|
||||||
|
result = expression_manager.get_expressions(part_path)
|
||||||
|
if result["success"]:
|
||||||
|
for name, expr in result["data"]["expressions"].items():
|
||||||
|
print(f"{name} = {expr['value']} {expr['units']}")
|
||||||
|
|
||||||
|
# Get mass properties
|
||||||
|
result = geometry_query.get_mass_properties(part_path)
|
||||||
|
if result["success"]:
|
||||||
|
print(f"Mass: {result['data']['mass']:.4f} kg")
|
||||||
|
print(f"Material: {result['data']['material']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Reference
|
||||||
|
|
||||||
|
### part_manager
|
||||||
|
|
||||||
|
Manage NX part files (open, close, save).
|
||||||
|
|
||||||
|
| Function | Description | Returns |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `open_part(path)` | Open an NX part file | Part info dict |
|
||||||
|
| `close_part(path)` | Close an open part | Success status |
|
||||||
|
| `save_part(path)` | Save a part | Success status |
|
||||||
|
| `save_part_as(path, new_path)` | Save with new name | Success status |
|
||||||
|
| `get_part_info(path)` | Get part metadata | Part info dict |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
from optimization_engine.hooks.nx_cad import part_manager
|
||||||
|
|
||||||
|
# Open a part
|
||||||
|
result = part_manager.open_part("C:/models/bracket.prt")
|
||||||
|
if result["success"]:
|
||||||
|
print(f"Opened: {result['data']['part_name']}")
|
||||||
|
print(f"Modified: {result['data']['is_modified']}")
|
||||||
|
|
||||||
|
# Save the part
|
||||||
|
result = part_manager.save_part("C:/models/bracket.prt")
|
||||||
|
|
||||||
|
# Save as new file
|
||||||
|
result = part_manager.save_part_as(
|
||||||
|
"C:/models/bracket.prt",
|
||||||
|
"C:/models/bracket_v2.prt"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### expression_manager
|
||||||
|
|
||||||
|
Get and set NX expressions (design parameters).
|
||||||
|
|
||||||
|
| Function | Description | Returns |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `get_expressions(path)` | Get all expressions | Dict of expressions |
|
||||||
|
| `get_expression(path, name)` | Get single expression | Expression dict |
|
||||||
|
| `set_expression(path, name, value)` | Set single expression | Success status |
|
||||||
|
| `set_expressions(path, dict)` | Set multiple expressions | Success status |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
from optimization_engine.hooks.nx_cad import expression_manager
|
||||||
|
|
||||||
|
part = "C:/models/bracket.prt"
|
||||||
|
|
||||||
|
# Get all expressions
|
||||||
|
result = expression_manager.get_expressions(part)
|
||||||
|
if result["success"]:
|
||||||
|
for name, expr in result["data"]["expressions"].items():
|
||||||
|
print(f"{name} = {expr['value']} {expr['units']}")
|
||||||
|
# Example output:
|
||||||
|
# thickness = 5.0 MilliMeter
|
||||||
|
# width = 50.0 MilliMeter
|
||||||
|
|
||||||
|
# Get specific expression
|
||||||
|
result = expression_manager.get_expression(part, "thickness")
|
||||||
|
if result["success"]:
|
||||||
|
print(f"Thickness: {result['data']['value']} {result['data']['units']}")
|
||||||
|
|
||||||
|
# Set single expression
|
||||||
|
result = expression_manager.set_expression(part, "thickness", 7.5)
|
||||||
|
|
||||||
|
# Set multiple expressions (batch update)
|
||||||
|
result = expression_manager.set_expressions(part, {
|
||||||
|
"thickness": 7.5,
|
||||||
|
"width": 60.0,
|
||||||
|
"height": 100.0
|
||||||
|
})
|
||||||
|
if result["success"]:
|
||||||
|
print(f"Updated {result['data']['update_count']} expressions")
|
||||||
|
```
|
||||||
|
|
||||||
|
### geometry_query
|
||||||
|
|
||||||
|
Query geometric properties (mass, volume, bodies).
|
||||||
|
|
||||||
|
| Function | Description | Returns |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `get_mass_properties(path)` | Get mass, volume, area, centroid | Properties dict |
|
||||||
|
| `get_bodies(path)` | Get body count and types | Bodies dict |
|
||||||
|
| `get_volume(path)` | Get total volume | Volume float |
|
||||||
|
| `get_surface_area(path)` | Get total surface area | Area float |
|
||||||
|
| `get_material(path)` | Get material name | Material string |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
from optimization_engine.hooks.nx_cad import geometry_query
|
||||||
|
|
||||||
|
part = "C:/models/bracket.prt"
|
||||||
|
|
||||||
|
# Get mass properties
|
||||||
|
result = geometry_query.get_mass_properties(part)
|
||||||
|
if result["success"]:
|
||||||
|
data = result["data"]
|
||||||
|
print(f"Mass: {data['mass']:.6f} {data['mass_unit']}")
|
||||||
|
print(f"Volume: {data['volume']:.2f} {data['volume_unit']}")
|
||||||
|
print(f"Surface Area: {data['surface_area']:.2f} {data['area_unit']}")
|
||||||
|
print(f"Centroid: ({data['centroid']['x']:.2f}, "
|
||||||
|
f"{data['centroid']['y']:.2f}, {data['centroid']['z']:.2f}) mm")
|
||||||
|
print(f"Material: {data['material']}")
|
||||||
|
# Example output:
|
||||||
|
# Mass: 0.109838 kg
|
||||||
|
# Volume: 39311.99 mm^3
|
||||||
|
# Surface Area: 10876.71 mm^2
|
||||||
|
# Centroid: (0.00, 42.30, 39.58) mm
|
||||||
|
# Material: Aluminum_2014
|
||||||
|
|
||||||
|
# Get body information
|
||||||
|
result = geometry_query.get_bodies(part)
|
||||||
|
if result["success"]:
|
||||||
|
print(f"Total bodies: {result['data']['count']}")
|
||||||
|
print(f"Solid bodies: {result['data']['solid_count']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### feature_manager
|
||||||
|
|
||||||
|
Suppress and unsuppress features for design exploration.
|
||||||
|
|
||||||
|
| Function | Description | Returns |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `get_features(path)` | List all features | Features list |
|
||||||
|
| `get_feature_status(path, name)` | Check if suppressed | Boolean |
|
||||||
|
| `suppress_feature(path, name)` | Suppress a feature | Success status |
|
||||||
|
| `unsuppress_feature(path, name)` | Unsuppress a feature | Success status |
|
||||||
|
| `suppress_features(path, names)` | Suppress multiple | Success status |
|
||||||
|
| `unsuppress_features(path, names)` | Unsuppress multiple | Success status |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
from optimization_engine.hooks.nx_cad import feature_manager
|
||||||
|
|
||||||
|
part = "C:/models/bracket.prt"
|
||||||
|
|
||||||
|
# List all features
|
||||||
|
result = feature_manager.get_features(part)
|
||||||
|
if result["success"]:
|
||||||
|
print(f"Found {result['data']['count']} features")
|
||||||
|
for feat in result["data"]["features"]:
|
||||||
|
status = "suppressed" if feat["is_suppressed"] else "active"
|
||||||
|
print(f" {feat['name']} ({feat['type']}): {status}")
|
||||||
|
|
||||||
|
# Suppress a feature
|
||||||
|
result = feature_manager.suppress_feature(part, "FILLET(3)")
|
||||||
|
if result["success"]:
|
||||||
|
print("Feature suppressed!")
|
||||||
|
|
||||||
|
# Unsuppress multiple features
|
||||||
|
result = feature_manager.unsuppress_features(part, ["FILLET(3)", "CHAMFER(1)"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Return Format
|
||||||
|
|
||||||
|
All hook functions return a consistent dictionary structure:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"success": bool, # True if operation succeeded
|
||||||
|
"error": str | None, # Error message if failed
|
||||||
|
"data": dict # Operation-specific results
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Handling:**
|
||||||
|
```python
|
||||||
|
result = expression_manager.get_expressions(part_path)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
print(f"Error: {result['error']}")
|
||||||
|
# Handle error...
|
||||||
|
else:
|
||||||
|
# Process result["data"]...
|
||||||
|
```
|
||||||
|
|
||||||
|
## NX Open API Reference
|
||||||
|
|
||||||
|
These hooks use the following NX Open APIs (verified via Siemens MCP documentation):
|
||||||
|
|
||||||
|
| Hook | NX Open API |
|
||||||
|
|------|-------------|
|
||||||
|
| Open part | `Session.Parts.OpenActiveDisplay()` |
|
||||||
|
| Close part | `Part.Close()` |
|
||||||
|
| Save part | `Part.Save()`, `Part.SaveAs()` |
|
||||||
|
| Get expressions | `Part.Expressions` collection |
|
||||||
|
| Set expression | `ExpressionCollection.Edit()` |
|
||||||
|
| Update model | `Session.UpdateManager.DoUpdate()` |
|
||||||
|
| Mass properties | `MeasureManager.NewMassProperties()` |
|
||||||
|
| Get bodies | `Part.Bodies` collection |
|
||||||
|
| Suppress feature | `Feature.Suppress()` |
|
||||||
|
| Unsuppress feature | `Feature.Unsuppress()` |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### NX Path
|
||||||
|
|
||||||
|
Set the NX installation path via environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
set NX_BIN_PATH=C:\Program Files\Siemens\NX2506\NXBIN
|
||||||
|
|
||||||
|
# Or in Python before importing
|
||||||
|
import os
|
||||||
|
os.environ["NX_BIN_PATH"] = r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeout
|
||||||
|
|
||||||
|
Journal execution has a default 2-minute timeout. For large parts, you may need to increase this in the hook source code.
|
||||||
|
|
||||||
|
## Integration with Atomizer
|
||||||
|
|
||||||
|
These hooks are designed to integrate with Atomizer's optimization workflow:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In run_optimization.py or custom extractor
|
||||||
|
|
||||||
|
from optimization_engine.hooks.nx_cad import expression_manager, geometry_query
|
||||||
|
|
||||||
|
def evaluate_design(part_path: str, params: dict) -> dict:
|
||||||
|
"""Evaluate a design point by updating NX model and extracting metrics."""
|
||||||
|
|
||||||
|
# 1. Update design parameters
|
||||||
|
result = expression_manager.set_expressions(part_path, params)
|
||||||
|
if not result["success"]:
|
||||||
|
raise RuntimeError(f"Failed to set expressions: {result['error']}")
|
||||||
|
|
||||||
|
# 2. Extract mass (objective)
|
||||||
|
result = geometry_query.get_mass_properties(part_path)
|
||||||
|
if not result["success"]:
|
||||||
|
raise RuntimeError(f"Failed to get mass: {result['error']}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"mass_kg": result["data"]["mass"],
|
||||||
|
"volume_mm3": result["data"]["volume"],
|
||||||
|
"material": result["data"]["material"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the test script to verify hooks work with your NX installation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Activate atomizer environment
|
||||||
|
conda activate atomizer
|
||||||
|
|
||||||
|
# Run tests with default bracket part
|
||||||
|
python -m optimization_engine.hooks.test_hooks
|
||||||
|
|
||||||
|
# Or specify a custom part
|
||||||
|
python -m optimization_engine.hooks.test_hooks "C:/path/to/your/part.prt"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Part file not found"
|
||||||
|
- Verify the path exists and is accessible
|
||||||
|
- Use forward slashes or raw strings: `r"C:\path\to\file.prt"`
|
||||||
|
|
||||||
|
### "Failed to open part"
|
||||||
|
- Ensure NX license is available
|
||||||
|
- Check `NX_BIN_PATH` environment variable
|
||||||
|
- Verify NX version compatibility
|
||||||
|
|
||||||
|
### "Expression not found"
|
||||||
|
- Expression names are case-sensitive
|
||||||
|
- Use `get_expressions()` to list available names
|
||||||
|
|
||||||
|
### Journal execution timeout
|
||||||
|
- Large parts may need longer timeout
|
||||||
|
- Check NX is not displaying modal dialogs
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0.0 | 2025-12-06 | Initial release with CAD hooks |
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [NX_OPEN_AUTOMATION_ROADMAP.md](../../docs/plans/NX_OPEN_AUTOMATION_ROADMAP.md) - Development roadmap
|
||||||
|
- [SYS_12_EXTRACTOR_LIBRARY.md](../../docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md) - Extractor catalog
|
||||||
|
- [NXJournaling.com](https://nxjournaling.com/) - NX Open examples
|
||||||
72
optimization_engine/hooks/__init__.py
Normal file
72
optimization_engine/hooks/__init__.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Atomizer NX Open Hooks
|
||||||
|
======================
|
||||||
|
|
||||||
|
Direct Python hooks for NX CAD/CAE operations via NX Open API.
|
||||||
|
|
||||||
|
This module provides a clean Python interface for manipulating NX parts
|
||||||
|
programmatically. Each hook executes NX journals via `run_journal.exe`
|
||||||
|
and returns structured JSON results.
|
||||||
|
|
||||||
|
Modules
|
||||||
|
-------
|
||||||
|
nx_cad : CAD manipulation hooks
|
||||||
|
- part_manager : Open, close, save parts
|
||||||
|
- expression_manager : Get/set design parameters
|
||||||
|
- geometry_query : Mass properties, bodies, volumes
|
||||||
|
- feature_manager : Suppress/unsuppress features
|
||||||
|
|
||||||
|
nx_cae : CAE/Simulation hooks (Phase 2)
|
||||||
|
- solver_manager : BDF export, solve simulations
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
-----------
|
||||||
|
>>> from optimization_engine.hooks.nx_cad import expression_manager
|
||||||
|
>>> result = expression_manager.get_expressions("C:/model.prt")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... for name, expr in result["data"]["expressions"].items():
|
||||||
|
... print(f"{name} = {expr['value']}")
|
||||||
|
|
||||||
|
>>> from optimization_engine.hooks.nx_cae import solver_manager
|
||||||
|
>>> result = solver_manager.get_bdf_from_solution_folder("C:/model.sim")
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
- Siemens NX 2506+ installed
|
||||||
|
- NX_BIN_PATH environment variable (or default path)
|
||||||
|
- Python 3.8+ with atomizer conda environment
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
- optimization_engine/hooks/README.md : Full documentation
|
||||||
|
- docs/plans/NX_OPEN_AUTOMATION_ROADMAP.md : Development roadmap
|
||||||
|
|
||||||
|
Version
|
||||||
|
-------
|
||||||
|
1.1.0 (2025-12-06) - Added nx_cae module with solver_manager
|
||||||
|
1.0.0 (2025-12-06) - Initial release with nx_cad hooks
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .nx_cad import (
|
||||||
|
part_manager,
|
||||||
|
expression_manager,
|
||||||
|
geometry_query,
|
||||||
|
feature_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .nx_cae import (
|
||||||
|
solver_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# CAD hooks
|
||||||
|
'part_manager',
|
||||||
|
'expression_manager',
|
||||||
|
'geometry_query',
|
||||||
|
'feature_manager',
|
||||||
|
# CAE hooks
|
||||||
|
'solver_manager',
|
||||||
|
]
|
||||||
|
|
||||||
|
__version__ = '1.1.0'
|
||||||
|
__author__ = 'Atomizer'
|
||||||
399
optimization_engine/hooks/examples.py
Normal file
399
optimization_engine/hooks/examples.py
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
"""
|
||||||
|
NX Open Hooks - Usage Examples
|
||||||
|
==============================
|
||||||
|
|
||||||
|
This file contains practical examples of using the NX Open hooks
|
||||||
|
for common optimization tasks.
|
||||||
|
|
||||||
|
Run examples:
|
||||||
|
python -m optimization_engine.hooks.examples
|
||||||
|
|
||||||
|
Or import specific examples:
|
||||||
|
from optimization_engine.hooks.examples import design_exploration_example
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
project_root = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
from optimization_engine.hooks.nx_cad import (
|
||||||
|
part_manager,
|
||||||
|
expression_manager,
|
||||||
|
geometry_query,
|
||||||
|
feature_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Example 1: Basic Expression Query
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def basic_expression_query(part_path: str):
|
||||||
|
"""
|
||||||
|
Example: Query all expressions from an NX part.
|
||||||
|
|
||||||
|
This is useful for discovering available design parameters
|
||||||
|
before setting up an optimization study.
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Example 1: Basic Expression Query")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
result = expression_manager.get_expressions(part_path)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
print(f"ERROR: {result['error']}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = result["data"]
|
||||||
|
print(f"\nFound {data['count']} expressions:\n")
|
||||||
|
|
||||||
|
# Print in a nice table format
|
||||||
|
print(f"{'Name':<25} {'Value':>12} {'Units':<15} {'RHS'}")
|
||||||
|
print("-" * 70)
|
||||||
|
|
||||||
|
for name, expr in data["expressions"].items():
|
||||||
|
units = expr.get("units") or ""
|
||||||
|
rhs = expr.get("rhs", "")
|
||||||
|
# Truncate RHS if it's a formula reference
|
||||||
|
if len(rhs) > 20:
|
||||||
|
rhs = rhs[:17] + "..."
|
||||||
|
print(f"{name:<25} {expr['value']:>12.4f} {units:<15} {rhs}")
|
||||||
|
|
||||||
|
return data["expressions"]
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Example 2: Mass Properties Extraction
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def mass_properties_example(part_path: str):
|
||||||
|
"""
|
||||||
|
Example: Extract mass properties from an NX part.
|
||||||
|
|
||||||
|
This is useful for mass optimization objectives.
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Example 2: Mass Properties Extraction")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
result = geometry_query.get_mass_properties(part_path)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
print(f"ERROR: {result['error']}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = result["data"]
|
||||||
|
|
||||||
|
print(f"\nMass Properties:")
|
||||||
|
print("-" * 40)
|
||||||
|
print(f" Mass: {data['mass']:.6f} {data['mass_unit']}")
|
||||||
|
print(f" Volume: {data['volume']:.2f} {data['volume_unit']}")
|
||||||
|
print(f" Surface Area: {data['surface_area']:.2f} {data['area_unit']}")
|
||||||
|
print(f" Material: {data['material'] or 'Not assigned'}")
|
||||||
|
|
||||||
|
centroid = data["centroid"]
|
||||||
|
print(f"\nCentroid (mm):")
|
||||||
|
print(f" X: {centroid['x']:.4f}")
|
||||||
|
print(f" Y: {centroid['y']:.4f}")
|
||||||
|
print(f" Z: {centroid['z']:.4f}")
|
||||||
|
|
||||||
|
if data.get("principal_moments"):
|
||||||
|
pm = data["principal_moments"]
|
||||||
|
print(f"\nPrincipal Moments of Inertia ({pm['unit']}):")
|
||||||
|
print(f" Ixx: {pm['Ixx']:.4f}")
|
||||||
|
print(f" Iyy: {pm['Iyy']:.4f}")
|
||||||
|
print(f" Izz: {pm['Izz']:.4f}")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Example 3: Design Parameter Update
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def design_update_example(part_path: str, dry_run: bool = True):
|
||||||
|
"""
|
||||||
|
Example: Update design parameters in an NX part.
|
||||||
|
|
||||||
|
This demonstrates the workflow for parametric optimization:
|
||||||
|
1. Read current values
|
||||||
|
2. Compute new values
|
||||||
|
3. Update the model
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Path to the NX part
|
||||||
|
dry_run: If True, only shows what would be changed (default)
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Example 3: Design Parameter Update")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Step 1: Get current expressions
|
||||||
|
result = expression_manager.get_expressions(part_path)
|
||||||
|
if not result["success"]:
|
||||||
|
print(f"ERROR: {result['error']}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
expressions = result["data"]["expressions"]
|
||||||
|
|
||||||
|
# Step 2: Find numeric expressions (potential design variables)
|
||||||
|
design_vars = {}
|
||||||
|
for name, expr in expressions.items():
|
||||||
|
# Skip linked expressions (RHS contains another expression name)
|
||||||
|
if expr.get("rhs") and not expr["rhs"].replace(".", "").replace("-", "").isdigit():
|
||||||
|
continue
|
||||||
|
# Only include length/angle expressions
|
||||||
|
if expr.get("units") in ["MilliMeter", "Degrees", None]:
|
||||||
|
design_vars[name] = expr["value"]
|
||||||
|
|
||||||
|
print(f"\nIdentified {len(design_vars)} potential design variables:")
|
||||||
|
for name, value in design_vars.items():
|
||||||
|
print(f" {name}: {value}")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print("\n[DRY RUN] Would update expressions (no changes made)")
|
||||||
|
|
||||||
|
# Example: increase all dimensions by 10%
|
||||||
|
new_values = {name: value * 1.1 for name, value in design_vars.items()}
|
||||||
|
|
||||||
|
print("\nProposed changes:")
|
||||||
|
for name, new_val in new_values.items():
|
||||||
|
old_val = design_vars[name]
|
||||||
|
print(f" {name}: {old_val:.4f} -> {new_val:.4f} (+10%)")
|
||||||
|
|
||||||
|
return new_values
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Actually update the model
|
||||||
|
new_values = {name: value * 1.1 for name, value in design_vars.items()}
|
||||||
|
|
||||||
|
print("\nUpdating expressions...")
|
||||||
|
result = expression_manager.set_expressions(part_path, new_values)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
print(f"SUCCESS: Updated {result['data']['update_count']} expressions")
|
||||||
|
if result["data"].get("errors"):
|
||||||
|
print(f"Warnings: {result['data']['errors']}")
|
||||||
|
else:
|
||||||
|
print(f"ERROR: {result['error']}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Example 4: Feature Exploration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def feature_exploration_example(part_path: str):
|
||||||
|
"""
|
||||||
|
Example: Explore and manipulate features.
|
||||||
|
|
||||||
|
This is useful for topological optimization where features
|
||||||
|
can be suppressed/unsuppressed to explore design space.
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Example 4: Feature Exploration")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
result = feature_manager.get_features(part_path)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
print(f"ERROR: {result['error']}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = result["data"]
|
||||||
|
|
||||||
|
print(f"\nFound {data['count']} features ({data['suppressed_count']} suppressed):\n")
|
||||||
|
|
||||||
|
print(f"{'Name':<30} {'Type':<20} {'Status'}")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
for feat in data["features"]:
|
||||||
|
status = "SUPPRESSED" if feat["is_suppressed"] else "Active"
|
||||||
|
print(f"{feat['name']:<30} {feat['type']:<20} {status}")
|
||||||
|
|
||||||
|
# Group by type
|
||||||
|
print("\n\nFeatures by type:")
|
||||||
|
print("-" * 40)
|
||||||
|
type_counts = {}
|
||||||
|
for feat in data["features"]:
|
||||||
|
feat_type = feat["type"]
|
||||||
|
type_counts[feat_type] = type_counts.get(feat_type, 0) + 1
|
||||||
|
|
||||||
|
for feat_type, count in sorted(type_counts.items(), key=lambda x: -x[1]):
|
||||||
|
print(f" {feat_type}: {count}")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Example 5: Optimization Objective Evaluation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def evaluate_design_point(part_path: str, parameters: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Example: Complete design evaluation workflow.
|
||||||
|
|
||||||
|
This demonstrates how hooks integrate into an optimization loop:
|
||||||
|
1. Update parameters
|
||||||
|
2. Extract objectives (mass, volume)
|
||||||
|
3. Return metrics
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Path to the NX part
|
||||||
|
parameters: Dict of parameter_name -> new_value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with mass_kg, volume_mm3, surface_area_mm2
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Example 5: Optimization Objective Evaluation")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
print(f"\nParameters to set:")
|
||||||
|
for name, value in parameters.items():
|
||||||
|
print(f" {name} = {value}")
|
||||||
|
|
||||||
|
# Step 1: Update parameters
|
||||||
|
print("\n[1/2] Updating design parameters...")
|
||||||
|
result = expression_manager.set_expressions(part_path, parameters)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
raise RuntimeError(f"Failed to set expressions: {result['error']}")
|
||||||
|
|
||||||
|
print(f" Updated {result['data']['update_count']} expressions")
|
||||||
|
|
||||||
|
# Step 2: Extract objectives
|
||||||
|
print("\n[2/2] Extracting mass properties...")
|
||||||
|
result = geometry_query.get_mass_properties(part_path)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
raise RuntimeError(f"Failed to get mass properties: {result['error']}")
|
||||||
|
|
||||||
|
data = result["data"]
|
||||||
|
|
||||||
|
# Return metrics
|
||||||
|
metrics = {
|
||||||
|
"mass_kg": data["mass"],
|
||||||
|
"volume_mm3": data["volume"],
|
||||||
|
"surface_area_mm2": data["surface_area"],
|
||||||
|
"material": data.get("material"),
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"\nObjective metrics:")
|
||||||
|
print(f" Mass: {metrics['mass_kg']:.6f} kg")
|
||||||
|
print(f" Volume: {metrics['volume_mm3']:.2f} mm^3")
|
||||||
|
print(f" Surface Area: {metrics['surface_area_mm2']:.2f} mm^2")
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Example 6: Batch Processing Multiple Parts
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def batch_mass_extraction(part_paths: list) -> list:
|
||||||
|
"""
|
||||||
|
Example: Extract mass from multiple parts.
|
||||||
|
|
||||||
|
Useful for comparing variants or processing a design library.
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Example 6: Batch Processing Multiple Parts")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i, part_path in enumerate(part_paths, 1):
|
||||||
|
print(f"\n[{i}/{len(part_paths)}] Processing: {Path(part_path).name}")
|
||||||
|
|
||||||
|
result = geometry_query.get_mass_properties(part_path)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
data = result["data"]
|
||||||
|
results.append({
|
||||||
|
"part": Path(part_path).name,
|
||||||
|
"mass_kg": data["mass"],
|
||||||
|
"volume_mm3": data["volume"],
|
||||||
|
"material": data.get("material"),
|
||||||
|
"success": True,
|
||||||
|
})
|
||||||
|
print(f" Mass: {data['mass']:.4f} kg, Material: {data.get('material')}")
|
||||||
|
else:
|
||||||
|
results.append({
|
||||||
|
"part": Path(part_path).name,
|
||||||
|
"error": result["error"],
|
||||||
|
"success": False,
|
||||||
|
})
|
||||||
|
print(f" ERROR: {result['error']}")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n" + "-" * 60)
|
||||||
|
print("Summary:")
|
||||||
|
successful = [r for r in results if r["success"]]
|
||||||
|
print(f" Processed: {len(successful)}/{len(part_paths)} parts")
|
||||||
|
|
||||||
|
if successful:
|
||||||
|
total_mass = sum(r["mass_kg"] for r in successful)
|
||||||
|
print(f" Total mass: {total_mass:.4f} kg")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Main - Run All Examples
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run all examples with a test part."""
|
||||||
|
|
||||||
|
# Default test part
|
||||||
|
default_part = project_root / "studies/bracket_stiffness_optimization_V3/1_setup/model/Bracket.prt"
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
part_path = sys.argv[1]
|
||||||
|
else:
|
||||||
|
part_path = str(default_part)
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("NX OPEN HOOKS - EXAMPLES")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"\nUsing part: {Path(part_path).name}")
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
print(f"\nERROR: Part file not found: {part_path}")
|
||||||
|
print("\nUsage: python -m optimization_engine.hooks.examples [part_path]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Run examples
|
||||||
|
try:
|
||||||
|
# Example 1: Query expressions
|
||||||
|
basic_expression_query(part_path)
|
||||||
|
|
||||||
|
# Example 2: Get mass properties
|
||||||
|
mass_properties_example(part_path)
|
||||||
|
|
||||||
|
# Example 3: Design update (dry run)
|
||||||
|
design_update_example(part_path, dry_run=True)
|
||||||
|
|
||||||
|
# Example 4: Feature exploration
|
||||||
|
feature_exploration_example(part_path)
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("ALL EXAMPLES COMPLETED SUCCESSFULLY!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nEXAMPLE FAILED: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
83
optimization_engine/hooks/nx_cad/__init__.py
Normal file
83
optimization_engine/hooks/nx_cad/__init__.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"""
|
||||||
|
NX CAD Hooks
|
||||||
|
============
|
||||||
|
|
||||||
|
Direct manipulation of NX CAD parts via NX Open Python API.
|
||||||
|
|
||||||
|
This submodule contains hooks for CAD-level operations on NX parts:
|
||||||
|
geometry, expressions, features, and part management.
|
||||||
|
|
||||||
|
Modules
|
||||||
|
-------
|
||||||
|
part_manager
|
||||||
|
Open, close, save, and query NX part files.
|
||||||
|
|
||||||
|
Functions:
|
||||||
|
- open_part(path) -> Open an NX part file
|
||||||
|
- close_part(path) -> Close an open part
|
||||||
|
- save_part(path) -> Save a part
|
||||||
|
- save_part_as(path, new_path) -> Save with new name
|
||||||
|
- get_part_info(path) -> Get part metadata
|
||||||
|
|
||||||
|
expression_manager
|
||||||
|
Get and set NX expressions (design parameters).
|
||||||
|
|
||||||
|
Functions:
|
||||||
|
- get_expressions(path) -> Get all expressions
|
||||||
|
- get_expression(path, name) -> Get single expression
|
||||||
|
- set_expression(path, name, value) -> Set single expression
|
||||||
|
- set_expressions(path, dict) -> Set multiple expressions
|
||||||
|
|
||||||
|
geometry_query
|
||||||
|
Query geometric properties (mass, volume, area, bodies).
|
||||||
|
|
||||||
|
Functions:
|
||||||
|
- get_mass_properties(path) -> Get mass, volume, area, centroid
|
||||||
|
- get_bodies(path) -> Get body count and types
|
||||||
|
- get_volume(path) -> Get total volume
|
||||||
|
- get_surface_area(path) -> Get total surface area
|
||||||
|
- get_material(path) -> Get material name
|
||||||
|
|
||||||
|
feature_manager
|
||||||
|
Suppress and unsuppress features for design exploration.
|
||||||
|
|
||||||
|
Functions:
|
||||||
|
- get_features(path) -> List all features
|
||||||
|
- get_feature_status(path, name) -> Check if suppressed
|
||||||
|
- suppress_feature(path, name) -> Suppress a feature
|
||||||
|
- unsuppress_feature(path, name) -> Unsuppress a feature
|
||||||
|
- suppress_features(path, names) -> Suppress multiple
|
||||||
|
- unsuppress_features(path, names) -> Unsuppress multiple
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
>>> from optimization_engine.hooks.nx_cad import geometry_query
|
||||||
|
>>> result = geometry_query.get_mass_properties("C:/model.prt")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Mass: {result['data']['mass']:.4f} kg")
|
||||||
|
... print(f"Material: {result['data']['material']}")
|
||||||
|
|
||||||
|
NX Open APIs Used
|
||||||
|
-----------------
|
||||||
|
- Session.Parts.OpenActiveDisplay() - Open parts
|
||||||
|
- Part.Close(), Part.Save(), Part.SaveAs() - Part operations
|
||||||
|
- Part.Expressions, ExpressionCollection.Edit() - Expressions
|
||||||
|
- MeasureManager.NewMassProperties() - Mass properties
|
||||||
|
- Part.Bodies - Body collection
|
||||||
|
- Feature.Suppress(), Feature.Unsuppress() - Feature control
|
||||||
|
- Session.UpdateManager.DoUpdate() - Model update
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import part_manager
|
||||||
|
from . import expression_manager
|
||||||
|
from . import geometry_query
|
||||||
|
from . import feature_manager
|
||||||
|
from . import model_introspection
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'part_manager',
|
||||||
|
'expression_manager',
|
||||||
|
'geometry_query',
|
||||||
|
'feature_manager',
|
||||||
|
'model_introspection',
|
||||||
|
]
|
||||||
566
optimization_engine/hooks/nx_cad/expression_manager.py
Normal file
566
optimization_engine/hooks/nx_cad/expression_manager.py
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
"""
|
||||||
|
NX Expression Manager Hook
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Provides Python functions to get and set NX expressions (parameters).
|
||||||
|
|
||||||
|
API Reference (verified via Siemens MCP docs):
|
||||||
|
- Part.Expressions() -> ExpressionCollection
|
||||||
|
- ExpressionCollection.Edit(expression, value)
|
||||||
|
- Expression.Name, Expression.Value, Expression.RightHandSide
|
||||||
|
- Expression.Units.Name
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from optimization_engine.hooks.nx_cad import expression_manager
|
||||||
|
|
||||||
|
# Get all expressions
|
||||||
|
result = expression_manager.get_expressions("C:/path/to/part.prt")
|
||||||
|
|
||||||
|
# Get specific expression
|
||||||
|
result = expression_manager.get_expression("C:/path/to/part.prt", "thickness")
|
||||||
|
|
||||||
|
# Set expression value
|
||||||
|
result = expression_manager.set_expression("C:/path/to/part.prt", "thickness", 5.0)
|
||||||
|
|
||||||
|
# Set multiple expressions
|
||||||
|
result = expression_manager.set_expressions("C:/path/to/part.prt", {
|
||||||
|
"thickness": 5.0,
|
||||||
|
"width": 10.0
|
||||||
|
})
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any, List, Tuple, Union
|
||||||
|
|
||||||
|
# NX installation path (configurable)
|
||||||
|
NX_BIN_PATH = os.environ.get(
|
||||||
|
"NX_BIN_PATH",
|
||||||
|
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Journal template for expression operations
|
||||||
|
EXPRESSION_OPERATIONS_JOURNAL = '''
|
||||||
|
# NX Open Python Journal - Expression Operations
|
||||||
|
# Auto-generated by Atomizer hooks
|
||||||
|
|
||||||
|
import NXOpen
|
||||||
|
import NXOpen.UF
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Execute expression operation based on command arguments."""
|
||||||
|
# Get the NX session
|
||||||
|
session = NXOpen.Session.GetSession()
|
||||||
|
|
||||||
|
# Parse arguments: operation, part_path, output_json, [extra_args...]
|
||||||
|
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
||||||
|
|
||||||
|
if len(args) < 3:
|
||||||
|
raise ValueError("Usage: script.py <operation> <part_path> <output_json> [args...]")
|
||||||
|
|
||||||
|
operation = args[0]
|
||||||
|
part_path = args[1]
|
||||||
|
output_json = args[2]
|
||||||
|
extra_args = args[3:] if len(args) > 3 else []
|
||||||
|
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ensure part is open
|
||||||
|
part = ensure_part_open(session, part_path)
|
||||||
|
|
||||||
|
if part is None:
|
||||||
|
result["error"] = f"Failed to open part: {part_path}"
|
||||||
|
elif operation == "get_all":
|
||||||
|
result = get_all_expressions(part)
|
||||||
|
elif operation == "get":
|
||||||
|
expr_name = extra_args[0] if extra_args else None
|
||||||
|
result = get_expression(part, expr_name)
|
||||||
|
elif operation == "set":
|
||||||
|
expr_name = extra_args[0] if len(extra_args) > 0 else None
|
||||||
|
expr_value = extra_args[1] if len(extra_args) > 1 else None
|
||||||
|
result = set_expression(session, part, expr_name, expr_value)
|
||||||
|
elif operation == "set_multiple":
|
||||||
|
# Extra args is a JSON string with name:value pairs
|
||||||
|
expr_dict = json.loads(extra_args[0]) if extra_args else {}
|
||||||
|
result = set_multiple_expressions(session, part, expr_dict)
|
||||||
|
else:
|
||||||
|
result["error"] = f"Unknown operation: {operation}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
result["error"] = str(e)
|
||||||
|
result["traceback"] = traceback.format_exc()
|
||||||
|
|
||||||
|
# Write result to output JSON
|
||||||
|
with open(output_json, 'w') as f:
|
||||||
|
json.dump(result, f, indent=2)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_part_open(session, part_path):
|
||||||
|
"""Ensure the part is open and return it."""
|
||||||
|
# Check if already open
|
||||||
|
part_path_normalized = os.path.normpath(part_path).lower()
|
||||||
|
|
||||||
|
for part in session.Parts:
|
||||||
|
if os.path.normpath(part.FullPath).lower() == part_path_normalized:
|
||||||
|
return part
|
||||||
|
|
||||||
|
# Need to open it
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set load options for the working directory
|
||||||
|
working_dir = os.path.dirname(part_path)
|
||||||
|
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||||
|
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||||
|
|
||||||
|
# Use OpenActiveDisplay instead of OpenBase for better compatibility
|
||||||
|
part, load_status = session.Parts.OpenActiveDisplay(
|
||||||
|
part_path,
|
||||||
|
NXOpen.DisplayPartOption.AllowAdditional
|
||||||
|
)
|
||||||
|
load_status.Dispose()
|
||||||
|
return part
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_expressions(part):
|
||||||
|
"""Get all expressions from a part.
|
||||||
|
|
||||||
|
NX Open API: Part.Expressions() -> ExpressionCollection
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
expressions = {}
|
||||||
|
|
||||||
|
for expr in part.Expressions:
|
||||||
|
try:
|
||||||
|
expr_data = {
|
||||||
|
"name": expr.Name,
|
||||||
|
"value": expr.Value,
|
||||||
|
"rhs": expr.RightHandSide,
|
||||||
|
"units": expr.Units.Name if expr.Units else None,
|
||||||
|
"type": expr.Type.ToString() if hasattr(expr.Type, 'ToString') else str(expr.Type),
|
||||||
|
}
|
||||||
|
expressions[expr.Name] = expr_data
|
||||||
|
except:
|
||||||
|
# Skip expressions that can't be read
|
||||||
|
pass
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"count": len(expressions),
|
||||||
|
"expressions": expressions
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_expression(part, expr_name):
|
||||||
|
"""Get a specific expression by name.
|
||||||
|
|
||||||
|
NX Open API: ExpressionCollection iteration, Expression properties
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
if not expr_name:
|
||||||
|
result["error"] = "Expression name is required"
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the expression by name
|
||||||
|
found_expr = None
|
||||||
|
for expr in part.Expressions:
|
||||||
|
if expr.Name == expr_name:
|
||||||
|
found_expr = expr
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_expr is None:
|
||||||
|
result["error"] = f"Expression not found: {expr_name}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"name": found_expr.Name,
|
||||||
|
"value": found_expr.Value,
|
||||||
|
"rhs": found_expr.RightHandSide,
|
||||||
|
"units": found_expr.Units.Name if found_expr.Units else None,
|
||||||
|
"type": found_expr.Type.ToString() if hasattr(found_expr.Type, 'ToString') else str(found_expr.Type),
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def set_expression(session, part, expr_name, expr_value):
|
||||||
|
"""Set an expression value.
|
||||||
|
|
||||||
|
NX Open API: ExpressionCollection.Edit(expression, new_rhs)
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
if not expr_name:
|
||||||
|
result["error"] = "Expression name is required"
|
||||||
|
return result
|
||||||
|
|
||||||
|
if expr_value is None:
|
||||||
|
result["error"] = "Expression value is required"
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the expression
|
||||||
|
found_expr = None
|
||||||
|
for expr in part.Expressions:
|
||||||
|
if expr.Name == expr_name:
|
||||||
|
found_expr = expr
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_expr is None:
|
||||||
|
result["error"] = f"Expression not found: {expr_name}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Set undo mark
|
||||||
|
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Edit Expression")
|
||||||
|
|
||||||
|
# Edit the expression
|
||||||
|
# The value should be a string for the RHS
|
||||||
|
new_rhs = str(expr_value)
|
||||||
|
part.Expressions.Edit(found_expr, new_rhs)
|
||||||
|
|
||||||
|
# Update the model
|
||||||
|
session.UpdateManager.DoUpdate(mark_id)
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"name": expr_name,
|
||||||
|
"old_value": found_expr.Value, # Note: this might be the new value after edit
|
||||||
|
"new_rhs": new_rhs,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def set_multiple_expressions(session, part, expr_dict):
|
||||||
|
"""Set multiple expressions at once.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: NX session
|
||||||
|
part: NX part
|
||||||
|
expr_dict: Dict of expression name -> value
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
if not expr_dict:
|
||||||
|
result["error"] = "No expressions provided"
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set undo mark for all changes
|
||||||
|
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Edit Multiple Expressions")
|
||||||
|
|
||||||
|
updated = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for expr_name, expr_value in expr_dict.items():
|
||||||
|
# Find the expression
|
||||||
|
found_expr = None
|
||||||
|
for expr in part.Expressions:
|
||||||
|
if expr.Name == expr_name:
|
||||||
|
found_expr = expr
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_expr is None:
|
||||||
|
errors.append(f"Expression not found: {expr_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Edit the expression
|
||||||
|
new_rhs = str(expr_value)
|
||||||
|
part.Expressions.Edit(found_expr, new_rhs)
|
||||||
|
updated.append({"name": expr_name, "value": expr_value})
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Failed to set {expr_name}: {str(e)}")
|
||||||
|
|
||||||
|
# Update the model
|
||||||
|
session.UpdateManager.DoUpdate(mark_id)
|
||||||
|
|
||||||
|
result["success"] = len(errors) == 0
|
||||||
|
result["data"] = {
|
||||||
|
"updated": updated,
|
||||||
|
"errors": errors,
|
||||||
|
"update_count": len(updated),
|
||||||
|
"error_count": len(errors),
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _get_run_journal_exe() -> str:
|
||||||
|
"""Get the path to run_journal.exe."""
|
||||||
|
return os.path.join(NX_BIN_PATH, "run_journal.exe")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_journal(journal_path: str, args: list) -> Tuple[bool, str]:
|
||||||
|
"""Run an NX journal with arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, output_or_error)
|
||||||
|
"""
|
||||||
|
run_journal = _get_run_journal_exe()
|
||||||
|
|
||||||
|
if not os.path.exists(run_journal):
|
||||||
|
return False, f"run_journal.exe not found at {run_journal}"
|
||||||
|
|
||||||
|
cmd = [run_journal, journal_path, "-args"] + args
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120 # 2 minute timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False, f"Journal execution failed: {result.stderr}"
|
||||||
|
|
||||||
|
return True, result.stdout
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Journal execution timed out"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_expression_operation(
|
||||||
|
operation: str,
|
||||||
|
part_path: str,
|
||||||
|
extra_args: list = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Execute an expression operation via NX journal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation: The operation to perform (get_all, get, set, set_multiple)
|
||||||
|
part_path: Path to the part file
|
||||||
|
extra_args: Additional arguments for the operation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with operation result
|
||||||
|
"""
|
||||||
|
# Create temporary journal file
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode='w',
|
||||||
|
suffix='.py',
|
||||||
|
delete=False
|
||||||
|
) as journal_file:
|
||||||
|
journal_file.write(EXPRESSION_OPERATIONS_JOURNAL)
|
||||||
|
journal_path = journal_file.name
|
||||||
|
|
||||||
|
# Create temporary output file
|
||||||
|
output_file = tempfile.NamedTemporaryFile(
|
||||||
|
mode='w',
|
||||||
|
suffix='.json',
|
||||||
|
delete=False
|
||||||
|
).name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build arguments
|
||||||
|
args = [operation, part_path, output_file]
|
||||||
|
if extra_args:
|
||||||
|
args.extend(extra_args)
|
||||||
|
|
||||||
|
# Run the journal
|
||||||
|
success, output = _run_journal(journal_path, args)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
return {"success": False, "error": output, "data": {}}
|
||||||
|
|
||||||
|
# Read the result
|
||||||
|
if os.path.exists(output_file):
|
||||||
|
with open(output_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
else:
|
||||||
|
return {"success": False, "error": "Output file not created", "data": {}}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup temporary files
|
||||||
|
if os.path.exists(journal_path):
|
||||||
|
os.unlink(journal_path)
|
||||||
|
if os.path.exists(output_file):
|
||||||
|
os.unlink(output_file)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Public API
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def get_expressions(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Get all expressions from an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with count and expressions dict
|
||||||
|
Each expression has: name, value, rhs, units, type
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = get_expressions("C:/models/bracket.prt")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... for name, expr in result["data"]["expressions"].items():
|
||||||
|
... print(f"{name} = {expr['value']} {expr['units']}")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_expression_operation("get_all", part_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_expression(part_path: str, expression_name: str) -> Dict[str, Any]:
|
||||||
|
"""Get a specific expression from an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
expression_name: Name of the expression
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with name, value, rhs, units, type
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = get_expression("C:/models/bracket.prt", "thickness")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"thickness = {result['data']['value']}")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_expression_operation("get", part_path, [expression_name])
|
||||||
|
|
||||||
|
|
||||||
|
def set_expression(
|
||||||
|
part_path: str,
|
||||||
|
expression_name: str,
|
||||||
|
value: Union[float, int, str]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Set an expression value in an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
expression_name: Name of the expression
|
||||||
|
value: New value (will be converted to string for RHS)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with name, old_value, new_rhs
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = set_expression("C:/models/bracket.prt", "thickness", 5.0)
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print("Expression updated!")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_expression_operation(
|
||||||
|
"set",
|
||||||
|
part_path,
|
||||||
|
[expression_name, str(value)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def set_expressions(
|
||||||
|
part_path: str,
|
||||||
|
expressions: Dict[str, Union[float, int, str]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Set multiple expressions in an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
expressions: Dict mapping expression names to values
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with updated list, errors list, counts
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = set_expressions("C:/models/bracket.prt", {
|
||||||
|
... "thickness": 5.0,
|
||||||
|
... "width": 10.0,
|
||||||
|
... "height": 15.0
|
||||||
|
... })
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Updated {result['data']['update_count']} expressions")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert expressions dict to JSON string
|
||||||
|
expr_json = json.dumps(expressions)
|
||||||
|
|
||||||
|
return _execute_expression_operation(
|
||||||
|
"set_multiple",
|
||||||
|
part_path,
|
||||||
|
[expr_json]
|
||||||
|
)
|
||||||
711
optimization_engine/hooks/nx_cad/feature_manager.py
Normal file
711
optimization_engine/hooks/nx_cad/feature_manager.py
Normal file
@@ -0,0 +1,711 @@
|
|||||||
|
"""
|
||||||
|
NX Feature Manager Hook
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Provides Python functions to manage NX features (suppress, unsuppress, etc.).
|
||||||
|
|
||||||
|
API Reference (verified via Siemens MCP docs):
|
||||||
|
- Part.Features() -> FeatureCollection
|
||||||
|
- Feature.Suppress() -> Suppresses the feature
|
||||||
|
- Feature.Unsuppress() -> Unsuppresses the feature
|
||||||
|
- Feature.Name, Feature.IsSuppressed
|
||||||
|
- Session.UpdateManager.DoUpdate() -> Update the model
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from optimization_engine.hooks.nx_cad import feature_manager
|
||||||
|
|
||||||
|
# Get all features
|
||||||
|
result = feature_manager.get_features("C:/path/to/part.prt")
|
||||||
|
|
||||||
|
# Suppress a feature
|
||||||
|
result = feature_manager.suppress_feature("C:/path/to/part.prt", "HOLE(1)")
|
||||||
|
|
||||||
|
# Unsuppress a feature
|
||||||
|
result = feature_manager.unsuppress_feature("C:/path/to/part.prt", "HOLE(1)")
|
||||||
|
|
||||||
|
# Get feature status
|
||||||
|
result = feature_manager.get_feature_status("C:/path/to/part.prt", "HOLE(1)")
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any, List, Tuple
|
||||||
|
|
||||||
|
# NX installation path (configurable)
|
||||||
|
NX_BIN_PATH = os.environ.get(
|
||||||
|
"NX_BIN_PATH",
|
||||||
|
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Journal template for feature operations
|
||||||
|
FEATURE_OPERATIONS_JOURNAL = '''
|
||||||
|
# NX Open Python Journal - Feature Operations
|
||||||
|
# Auto-generated by Atomizer hooks
|
||||||
|
#
|
||||||
|
# Based on Siemens NX Open Python API:
|
||||||
|
# - Part.Features()
|
||||||
|
# - Feature.Suppress() / Feature.Unsuppress()
|
||||||
|
# - Feature.Name, Feature.IsSuppressed
|
||||||
|
|
||||||
|
import NXOpen
|
||||||
|
import NXOpen.Features
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Execute feature operation based on command arguments."""
|
||||||
|
# Get the NX session
|
||||||
|
session = NXOpen.Session.GetSession()
|
||||||
|
|
||||||
|
# Parse arguments: operation, part_path, output_json, [extra_args...]
|
||||||
|
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
||||||
|
|
||||||
|
if len(args) < 3:
|
||||||
|
raise ValueError("Usage: script.py <operation> <part_path> <output_json> [args...]")
|
||||||
|
|
||||||
|
operation = args[0]
|
||||||
|
part_path = args[1]
|
||||||
|
output_json = args[2]
|
||||||
|
extra_args = args[3:] if len(args) > 3 else []
|
||||||
|
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ensure part is open
|
||||||
|
part = ensure_part_open(session, part_path)
|
||||||
|
|
||||||
|
if part is None:
|
||||||
|
result["error"] = f"Failed to open part: {part_path}"
|
||||||
|
elif operation == "get_all":
|
||||||
|
result = get_all_features(part)
|
||||||
|
elif operation == "get_status":
|
||||||
|
feature_name = extra_args[0] if extra_args else None
|
||||||
|
result = get_feature_status(part, feature_name)
|
||||||
|
elif operation == "suppress":
|
||||||
|
feature_name = extra_args[0] if extra_args else None
|
||||||
|
result = suppress_feature(session, part, feature_name)
|
||||||
|
elif operation == "unsuppress":
|
||||||
|
feature_name = extra_args[0] if extra_args else None
|
||||||
|
result = unsuppress_feature(session, part, feature_name)
|
||||||
|
elif operation == "suppress_multiple":
|
||||||
|
feature_names = json.loads(extra_args[0]) if extra_args else []
|
||||||
|
result = suppress_multiple_features(session, part, feature_names)
|
||||||
|
elif operation == "unsuppress_multiple":
|
||||||
|
feature_names = json.loads(extra_args[0]) if extra_args else []
|
||||||
|
result = unsuppress_multiple_features(session, part, feature_names)
|
||||||
|
else:
|
||||||
|
result["error"] = f"Unknown operation: {operation}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
result["error"] = str(e)
|
||||||
|
result["traceback"] = traceback.format_exc()
|
||||||
|
|
||||||
|
# Write result to output JSON
|
||||||
|
with open(output_json, 'w') as f:
|
||||||
|
json.dump(result, f, indent=2)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_part_open(session, part_path):
|
||||||
|
"""Ensure the part is open and return it."""
|
||||||
|
# Check if already open
|
||||||
|
part_path_normalized = os.path.normpath(part_path).lower()
|
||||||
|
|
||||||
|
for part in session.Parts:
|
||||||
|
if os.path.normpath(part.FullPath).lower() == part_path_normalized:
|
||||||
|
return part
|
||||||
|
|
||||||
|
# Need to open it
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set load options for the working directory
|
||||||
|
working_dir = os.path.dirname(part_path)
|
||||||
|
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||||
|
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||||
|
|
||||||
|
# Use OpenActiveDisplay instead of OpenBase for better compatibility
|
||||||
|
part, load_status = session.Parts.OpenActiveDisplay(
|
||||||
|
part_path,
|
||||||
|
NXOpen.DisplayPartOption.AllowAdditional
|
||||||
|
)
|
||||||
|
load_status.Dispose()
|
||||||
|
return part
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_feature_by_name(part, feature_name):
|
||||||
|
"""Find a feature by name."""
|
||||||
|
for feature in part.Features:
|
||||||
|
if feature.Name == feature_name:
|
||||||
|
return feature
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_features(part):
|
||||||
|
"""Get all features from a part.
|
||||||
|
|
||||||
|
NX Open API: Part.Features()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
features = []
|
||||||
|
|
||||||
|
for feature in part.Features:
|
||||||
|
try:
|
||||||
|
feature_data = {
|
||||||
|
"name": feature.Name,
|
||||||
|
"type": feature.FeatureType,
|
||||||
|
"is_suppressed": feature.IsSuppressed,
|
||||||
|
"is_internal": feature.IsInternal,
|
||||||
|
}
|
||||||
|
features.append(feature_data)
|
||||||
|
except:
|
||||||
|
# Skip features that can't be read
|
||||||
|
pass
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"count": len(features),
|
||||||
|
"suppressed_count": sum(1 for f in features if f["is_suppressed"]),
|
||||||
|
"features": features
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_feature_status(part, feature_name):
|
||||||
|
"""Get status of a specific feature.
|
||||||
|
|
||||||
|
NX Open API: Feature properties
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
if not feature_name:
|
||||||
|
result["error"] = "Feature name is required"
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
feature = find_feature_by_name(part, feature_name)
|
||||||
|
|
||||||
|
if feature is None:
|
||||||
|
result["error"] = f"Feature not found: {feature_name}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"name": feature.Name,
|
||||||
|
"type": feature.FeatureType,
|
||||||
|
"is_suppressed": feature.IsSuppressed,
|
||||||
|
"is_internal": feature.IsInternal,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def suppress_feature(session, part, feature_name):
|
||||||
|
"""Suppress a feature.
|
||||||
|
|
||||||
|
NX Open API: Feature.Suppress()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
if not feature_name:
|
||||||
|
result["error"] = "Feature name is required"
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
feature = find_feature_by_name(part, feature_name)
|
||||||
|
|
||||||
|
if feature is None:
|
||||||
|
result["error"] = f"Feature not found: {feature_name}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
if feature.IsSuppressed:
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"name": feature_name,
|
||||||
|
"action": "already_suppressed",
|
||||||
|
"is_suppressed": True
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Set undo mark
|
||||||
|
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Suppress Feature")
|
||||||
|
|
||||||
|
# Suppress the feature
|
||||||
|
feature.Suppress()
|
||||||
|
|
||||||
|
# Update the model
|
||||||
|
session.UpdateManager.DoUpdate(mark_id)
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"name": feature_name,
|
||||||
|
"action": "suppressed",
|
||||||
|
"is_suppressed": True
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def unsuppress_feature(session, part, feature_name):
|
||||||
|
"""Unsuppress a feature.
|
||||||
|
|
||||||
|
NX Open API: Feature.Unsuppress()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
if not feature_name:
|
||||||
|
result["error"] = "Feature name is required"
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
feature = find_feature_by_name(part, feature_name)
|
||||||
|
|
||||||
|
if feature is None:
|
||||||
|
result["error"] = f"Feature not found: {feature_name}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
if not feature.IsSuppressed:
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"name": feature_name,
|
||||||
|
"action": "already_unsuppressed",
|
||||||
|
"is_suppressed": False
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Set undo mark
|
||||||
|
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Unsuppress Feature")
|
||||||
|
|
||||||
|
# Unsuppress the feature
|
||||||
|
feature.Unsuppress()
|
||||||
|
|
||||||
|
# Update the model
|
||||||
|
session.UpdateManager.DoUpdate(mark_id)
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"name": feature_name,
|
||||||
|
"action": "unsuppressed",
|
||||||
|
"is_suppressed": False
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def suppress_multiple_features(session, part, feature_names):
|
||||||
|
"""Suppress multiple features.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: NX session
|
||||||
|
part: NX part
|
||||||
|
feature_names: List of feature names to suppress
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
if not feature_names:
|
||||||
|
result["error"] = "No feature names provided"
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set undo mark for all changes
|
||||||
|
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Suppress Multiple Features")
|
||||||
|
|
||||||
|
suppressed = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for feature_name in feature_names:
|
||||||
|
feature = find_feature_by_name(part, feature_name)
|
||||||
|
|
||||||
|
if feature is None:
|
||||||
|
errors.append(f"Feature not found: {feature_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not feature.IsSuppressed:
|
||||||
|
feature.Suppress()
|
||||||
|
suppressed.append(feature_name)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Failed to suppress {feature_name}: {str(e)}")
|
||||||
|
|
||||||
|
# Update the model
|
||||||
|
session.UpdateManager.DoUpdate(mark_id)
|
||||||
|
|
||||||
|
result["success"] = len(errors) == 0
|
||||||
|
result["data"] = {
|
||||||
|
"suppressed": suppressed,
|
||||||
|
"errors": errors,
|
||||||
|
"suppressed_count": len(suppressed),
|
||||||
|
"error_count": len(errors),
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def unsuppress_multiple_features(session, part, feature_names):
|
||||||
|
"""Unsuppress multiple features.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: NX session
|
||||||
|
part: NX part
|
||||||
|
feature_names: List of feature names to unsuppress
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
if not feature_names:
|
||||||
|
result["error"] = "No feature names provided"
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set undo mark for all changes
|
||||||
|
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Unsuppress Multiple Features")
|
||||||
|
|
||||||
|
unsuppressed = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for feature_name in feature_names:
|
||||||
|
feature = find_feature_by_name(part, feature_name)
|
||||||
|
|
||||||
|
if feature is None:
|
||||||
|
errors.append(f"Feature not found: {feature_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if feature.IsSuppressed:
|
||||||
|
feature.Unsuppress()
|
||||||
|
unsuppressed.append(feature_name)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Failed to unsuppress {feature_name}: {str(e)}")
|
||||||
|
|
||||||
|
# Update the model
|
||||||
|
session.UpdateManager.DoUpdate(mark_id)
|
||||||
|
|
||||||
|
result["success"] = len(errors) == 0
|
||||||
|
result["data"] = {
|
||||||
|
"unsuppressed": unsuppressed,
|
||||||
|
"errors": errors,
|
||||||
|
"unsuppressed_count": len(unsuppressed),
|
||||||
|
"error_count": len(errors),
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _get_run_journal_exe() -> str:
|
||||||
|
"""Get the path to run_journal.exe."""
|
||||||
|
return os.path.join(NX_BIN_PATH, "run_journal.exe")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_journal(journal_path: str, args: list) -> Tuple[bool, str]:
|
||||||
|
"""Run an NX journal with arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, output_or_error)
|
||||||
|
"""
|
||||||
|
run_journal = _get_run_journal_exe()
|
||||||
|
|
||||||
|
if not os.path.exists(run_journal):
|
||||||
|
return False, f"run_journal.exe not found at {run_journal}"
|
||||||
|
|
||||||
|
cmd = [run_journal, journal_path, "-args"] + args
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120 # 2 minute timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False, f"Journal execution failed: {result.stderr}"
|
||||||
|
|
||||||
|
return True, result.stdout
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Journal execution timed out"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_feature_operation(
|
||||||
|
operation: str,
|
||||||
|
part_path: str,
|
||||||
|
extra_args: list = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Execute a feature operation via NX journal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation: The operation to perform
|
||||||
|
part_path: Path to the part file
|
||||||
|
extra_args: Additional arguments for the operation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with operation result
|
||||||
|
"""
|
||||||
|
# Create temporary journal file
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode='w',
|
||||||
|
suffix='.py',
|
||||||
|
delete=False
|
||||||
|
) as journal_file:
|
||||||
|
journal_file.write(FEATURE_OPERATIONS_JOURNAL)
|
||||||
|
journal_path = journal_file.name
|
||||||
|
|
||||||
|
# Create temporary output file
|
||||||
|
output_file = tempfile.NamedTemporaryFile(
|
||||||
|
mode='w',
|
||||||
|
suffix='.json',
|
||||||
|
delete=False
|
||||||
|
).name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build arguments
|
||||||
|
args = [operation, part_path, output_file]
|
||||||
|
if extra_args:
|
||||||
|
args.extend(extra_args)
|
||||||
|
|
||||||
|
# Run the journal
|
||||||
|
success, output = _run_journal(journal_path, args)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
return {"success": False, "error": output, "data": {}}
|
||||||
|
|
||||||
|
# Read the result
|
||||||
|
if os.path.exists(output_file):
|
||||||
|
with open(output_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
else:
|
||||||
|
return {"success": False, "error": "Output file not created", "data": {}}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup temporary files
|
||||||
|
if os.path.exists(journal_path):
|
||||||
|
os.unlink(journal_path)
|
||||||
|
if os.path.exists(output_file):
|
||||||
|
os.unlink(output_file)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Public API
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def get_features(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Get all features from an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with count, suppressed_count, features list
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = get_features("C:/models/bracket.prt")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... for f in result["data"]["features"]:
|
||||||
|
... status = "suppressed" if f["is_suppressed"] else "active"
|
||||||
|
... print(f"{f['name']} ({f['type']}): {status}")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_feature_operation("get_all", part_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_feature_status(part_path: str, feature_name: str) -> Dict[str, Any]:
|
||||||
|
"""Get status of a specific feature.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
feature_name: Name of the feature
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with name, type, is_suppressed, is_internal
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = get_feature_status("C:/models/bracket.prt", "HOLE(1)")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Suppressed: {result['data']['is_suppressed']}")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_feature_operation("get_status", part_path, [feature_name])
|
||||||
|
|
||||||
|
|
||||||
|
def suppress_feature(part_path: str, feature_name: str) -> Dict[str, Any]:
|
||||||
|
"""Suppress a feature in an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
feature_name: Name of the feature to suppress
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with name, action, is_suppressed
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = suppress_feature("C:/models/bracket.prt", "HOLE(1)")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Feature {result['data']['action']}")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_feature_operation("suppress", part_path, [feature_name])
|
||||||
|
|
||||||
|
|
||||||
|
def unsuppress_feature(part_path: str, feature_name: str) -> Dict[str, Any]:
|
||||||
|
"""Unsuppress a feature in an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
feature_name: Name of the feature to unsuppress
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with name, action, is_suppressed
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = unsuppress_feature("C:/models/bracket.prt", "HOLE(1)")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Feature {result['data']['action']}")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_feature_operation("unsuppress", part_path, [feature_name])
|
||||||
|
|
||||||
|
|
||||||
|
def suppress_features(part_path: str, feature_names: List[str]) -> Dict[str, Any]:
|
||||||
|
"""Suppress multiple features in an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
feature_names: List of feature names to suppress
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with suppressed list, errors list, counts
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = suppress_features("C:/models/bracket.prt", ["HOLE(1)", "HOLE(2)"])
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Suppressed {result['data']['suppressed_count']} features")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert list to JSON string
|
||||||
|
names_json = json.dumps(feature_names)
|
||||||
|
|
||||||
|
return _execute_feature_operation("suppress_multiple", part_path, [names_json])
|
||||||
|
|
||||||
|
|
||||||
|
def unsuppress_features(part_path: str, feature_names: List[str]) -> Dict[str, Any]:
|
||||||
|
"""Unsuppress multiple features in an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
feature_names: List of feature names to unsuppress
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with unsuppressed list, errors list, counts
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = unsuppress_features("C:/models/bracket.prt", ["HOLE(1)", "HOLE(2)"])
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Unsuppressed {result['data']['unsuppressed_count']} features")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert list to JSON string
|
||||||
|
names_json = json.dumps(feature_names)
|
||||||
|
|
||||||
|
return _execute_feature_operation("unsuppress_multiple", part_path, [names_json])
|
||||||
667
optimization_engine/hooks/nx_cad/geometry_query.py
Normal file
667
optimization_engine/hooks/nx_cad/geometry_query.py
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
"""
|
||||||
|
NX Geometry Query Hook
|
||||||
|
======================
|
||||||
|
|
||||||
|
Provides Python functions to query geometry properties from NX parts.
|
||||||
|
|
||||||
|
API Reference (verified via Siemens MCP docs):
|
||||||
|
- Part.MeasureManager() -> Returns measure manager for this part
|
||||||
|
- MeasureManager.NewMassProperties() -> Create mass properties measurement
|
||||||
|
- Part.Bodies() -> BodyCollection (solid bodies in the part)
|
||||||
|
- Body.GetPhysicalMaterial() -> Get material assigned to body
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from optimization_engine.hooks.nx_cad import geometry_query
|
||||||
|
|
||||||
|
# Get mass properties
|
||||||
|
result = geometry_query.get_mass_properties("C:/path/to/part.prt")
|
||||||
|
|
||||||
|
# Get body info
|
||||||
|
result = geometry_query.get_bodies("C:/path/to/part.prt")
|
||||||
|
|
||||||
|
# Get volume
|
||||||
|
result = geometry_query.get_volume("C:/path/to/part.prt")
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any, List, Tuple
|
||||||
|
|
||||||
|
# NX installation path (configurable)
|
||||||
|
NX_BIN_PATH = os.environ.get(
|
||||||
|
"NX_BIN_PATH",
|
||||||
|
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Journal template for geometry query operations
|
||||||
|
GEOMETRY_QUERY_JOURNAL = '''
|
||||||
|
# NX Open Python Journal - Geometry Query Operations
|
||||||
|
# Auto-generated by Atomizer hooks
|
||||||
|
#
|
||||||
|
# Based on Siemens NX Open Python API:
|
||||||
|
# - MeasureManager.NewMassProperties()
|
||||||
|
# - BodyCollection
|
||||||
|
# - Body.GetPhysicalMaterial()
|
||||||
|
|
||||||
|
import NXOpen
|
||||||
|
import NXOpen.UF
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import math
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Execute geometry query operation based on command arguments."""
|
||||||
|
# Get the NX session
|
||||||
|
session = NXOpen.Session.GetSession()
|
||||||
|
|
||||||
|
# Parse arguments: operation, part_path, output_json, [extra_args...]
|
||||||
|
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
||||||
|
|
||||||
|
if len(args) < 3:
|
||||||
|
raise ValueError("Usage: script.py <operation> <part_path> <output_json> [args...]")
|
||||||
|
|
||||||
|
operation = args[0]
|
||||||
|
part_path = args[1]
|
||||||
|
output_json = args[2]
|
||||||
|
extra_args = args[3:] if len(args) > 3 else []
|
||||||
|
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ensure part is open
|
||||||
|
part = ensure_part_open(session, part_path)
|
||||||
|
|
||||||
|
if part is None:
|
||||||
|
result["error"] = f"Failed to open part: {part_path}"
|
||||||
|
elif operation == "mass_properties":
|
||||||
|
result = get_mass_properties(part)
|
||||||
|
elif operation == "bodies":
|
||||||
|
result = get_bodies(part)
|
||||||
|
elif operation == "volume":
|
||||||
|
result = get_volume(part)
|
||||||
|
elif operation == "surface_area":
|
||||||
|
result = get_surface_area(part)
|
||||||
|
elif operation == "material":
|
||||||
|
result = get_material(part)
|
||||||
|
else:
|
||||||
|
result["error"] = f"Unknown operation: {operation}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
result["error"] = str(e)
|
||||||
|
result["traceback"] = traceback.format_exc()
|
||||||
|
|
||||||
|
# Write result to output JSON
|
||||||
|
with open(output_json, 'w') as f:
|
||||||
|
json.dump(result, f, indent=2)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_part_open(session, part_path):
|
||||||
|
"""Ensure the part is open and return it."""
|
||||||
|
# Check if already open
|
||||||
|
part_path_normalized = os.path.normpath(part_path).lower()
|
||||||
|
|
||||||
|
for part in session.Parts:
|
||||||
|
if os.path.normpath(part.FullPath).lower() == part_path_normalized:
|
||||||
|
return part
|
||||||
|
|
||||||
|
# Need to open it
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set load options for the working directory
|
||||||
|
working_dir = os.path.dirname(part_path)
|
||||||
|
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||||
|
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||||
|
|
||||||
|
# Use OpenActiveDisplay instead of OpenBase for better compatibility
|
||||||
|
part, load_status = session.Parts.OpenActiveDisplay(
|
||||||
|
part_path,
|
||||||
|
NXOpen.DisplayPartOption.AllowAdditional
|
||||||
|
)
|
||||||
|
load_status.Dispose()
|
||||||
|
return part
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_solid_bodies(part):
|
||||||
|
"""Get all solid bodies from a part."""
|
||||||
|
solid_bodies = []
|
||||||
|
for body in part.Bodies:
|
||||||
|
if body.IsSolidBody:
|
||||||
|
solid_bodies.append(body)
|
||||||
|
return solid_bodies
|
||||||
|
|
||||||
|
|
||||||
|
def get_mass_properties(part):
|
||||||
|
"""Get mass properties from a part.
|
||||||
|
|
||||||
|
NX Open API: MeasureManager.NewMassProperties()
|
||||||
|
|
||||||
|
Returns mass, volume, surface area, centroid, and inertia properties.
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get solid bodies
|
||||||
|
solid_bodies = get_solid_bodies(part)
|
||||||
|
|
||||||
|
if not solid_bodies:
|
||||||
|
result["error"] = "No solid bodies found in part"
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Get measure manager
|
||||||
|
measure_manager = part.MeasureManager
|
||||||
|
|
||||||
|
# Get units - use base units array like the working journal
|
||||||
|
uc = part.UnitCollection
|
||||||
|
mass_units = [
|
||||||
|
uc.GetBase("Area"),
|
||||||
|
uc.GetBase("Volume"),
|
||||||
|
uc.GetBase("Mass"),
|
||||||
|
uc.GetBase("Length")
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create mass properties measurement
|
||||||
|
# Signature: NewMassProperties(mass_units, accuracy, objects)
|
||||||
|
mass_props = measure_manager.NewMassProperties(mass_units, 0.99, solid_bodies)
|
||||||
|
|
||||||
|
# Get properties
|
||||||
|
mass = mass_props.Mass
|
||||||
|
volume = mass_props.Volume
|
||||||
|
area = mass_props.Area
|
||||||
|
|
||||||
|
# Get centroid
|
||||||
|
centroid = mass_props.Centroid
|
||||||
|
centroid_x = centroid.X
|
||||||
|
centroid_y = centroid.Y
|
||||||
|
centroid_z = centroid.Z
|
||||||
|
|
||||||
|
# Get principal moments of inertia (may not be available)
|
||||||
|
ixx = 0.0
|
||||||
|
iyy = 0.0
|
||||||
|
izz = 0.0
|
||||||
|
try:
|
||||||
|
principal_moments = mass_props.PrincipalMomentsOfInertia
|
||||||
|
ixx = principal_moments[0]
|
||||||
|
iyy = principal_moments[1]
|
||||||
|
izz = principal_moments[2]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get material info from first body via attributes
|
||||||
|
material_name = None
|
||||||
|
density = None
|
||||||
|
try:
|
||||||
|
# Try body attributes (NX stores material as attribute)
|
||||||
|
attrs = solid_bodies[0].GetUserAttributes()
|
||||||
|
for attr in attrs:
|
||||||
|
if 'material' in attr.Title.lower():
|
||||||
|
material_name = attr.StringValue
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"mass": mass,
|
||||||
|
"mass_unit": "kg",
|
||||||
|
"volume": volume,
|
||||||
|
"volume_unit": "mm^3",
|
||||||
|
"surface_area": area,
|
||||||
|
"area_unit": "mm^2",
|
||||||
|
"centroid": {
|
||||||
|
"x": centroid_x,
|
||||||
|
"y": centroid_y,
|
||||||
|
"z": centroid_z,
|
||||||
|
"unit": "mm"
|
||||||
|
},
|
||||||
|
"principal_moments": {
|
||||||
|
"Ixx": ixx,
|
||||||
|
"Iyy": iyy,
|
||||||
|
"Izz": izz,
|
||||||
|
"unit": "kg*mm^2"
|
||||||
|
},
|
||||||
|
"material": material_name,
|
||||||
|
"density": density,
|
||||||
|
"body_count": len(solid_bodies)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
result["error"] = str(e)
|
||||||
|
result["traceback"] = traceback.format_exc()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_bodies(part):
|
||||||
|
"""Get information about all bodies in the part.
|
||||||
|
|
||||||
|
NX Open API: Part.Bodies()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
bodies_info = []
|
||||||
|
|
||||||
|
for body in part.Bodies:
|
||||||
|
body_data = {
|
||||||
|
"name": body.Name if hasattr(body, 'Name') else None,
|
||||||
|
"is_solid": body.IsSolidBody,
|
||||||
|
"is_sheet": body.IsSheetBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to get material
|
||||||
|
try:
|
||||||
|
phys_mat = body.GetPhysicalMaterial()
|
||||||
|
if phys_mat:
|
||||||
|
body_data["material"] = phys_mat.Name
|
||||||
|
except:
|
||||||
|
body_data["material"] = None
|
||||||
|
|
||||||
|
bodies_info.append(body_data)
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"count": len(bodies_info),
|
||||||
|
"solid_count": sum(1 for b in bodies_info if b["is_solid"]),
|
||||||
|
"sheet_count": sum(1 for b in bodies_info if b["is_sheet"]),
|
||||||
|
"bodies": bodies_info
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_volume(part):
|
||||||
|
"""Get total volume of all solid bodies.
|
||||||
|
|
||||||
|
NX Open API: MeasureManager.NewMassProperties()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
solid_bodies = get_solid_bodies(part)
|
||||||
|
|
||||||
|
if not solid_bodies:
|
||||||
|
result["error"] = "No solid bodies found in part"
|
||||||
|
return result
|
||||||
|
|
||||||
|
measure_manager = part.MeasureManager
|
||||||
|
units = part.UnitCollection
|
||||||
|
|
||||||
|
body_array = [NXOpen.IBody.Wrap(body) for body in solid_bodies]
|
||||||
|
|
||||||
|
mass_props = measure_manager.NewMassProperties(
|
||||||
|
units.FindObject("SquareMilliMeter"),
|
||||||
|
units.FindObject("CubicMillimeter"),
|
||||||
|
units.FindObject("Kilogram"),
|
||||||
|
body_array
|
||||||
|
)
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"volume": mass_props.Volume,
|
||||||
|
"unit": "mm^3",
|
||||||
|
"body_count": len(solid_bodies)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_surface_area(part):
|
||||||
|
"""Get total surface area of all solid bodies.
|
||||||
|
|
||||||
|
NX Open API: MeasureManager.NewMassProperties()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
solid_bodies = get_solid_bodies(part)
|
||||||
|
|
||||||
|
if not solid_bodies:
|
||||||
|
result["error"] = "No solid bodies found in part"
|
||||||
|
return result
|
||||||
|
|
||||||
|
measure_manager = part.MeasureManager
|
||||||
|
units = part.UnitCollection
|
||||||
|
|
||||||
|
body_array = [NXOpen.IBody.Wrap(body) for body in solid_bodies]
|
||||||
|
|
||||||
|
mass_props = measure_manager.NewMassProperties(
|
||||||
|
units.FindObject("SquareMilliMeter"),
|
||||||
|
units.FindObject("CubicMillimeter"),
|
||||||
|
units.FindObject("Kilogram"),
|
||||||
|
body_array
|
||||||
|
)
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"surface_area": mass_props.Area,
|
||||||
|
"unit": "mm^2",
|
||||||
|
"body_count": len(solid_bodies)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_material(part):
|
||||||
|
"""Get material information from bodies in the part.
|
||||||
|
|
||||||
|
NX Open API: Body.GetPhysicalMaterial()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
solid_bodies = get_solid_bodies(part)
|
||||||
|
|
||||||
|
if not solid_bodies:
|
||||||
|
result["error"] = "No solid bodies found in part"
|
||||||
|
return result
|
||||||
|
|
||||||
|
materials = {}
|
||||||
|
|
||||||
|
for body in solid_bodies:
|
||||||
|
try:
|
||||||
|
phys_mat = body.GetPhysicalMaterial()
|
||||||
|
if phys_mat:
|
||||||
|
mat_name = phys_mat.Name
|
||||||
|
if mat_name not in materials:
|
||||||
|
mat_data = {"name": mat_name}
|
||||||
|
# Try to get properties
|
||||||
|
try:
|
||||||
|
mat_data["density"] = phys_mat.GetRealPropertyValue("Density")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
mat_data["youngs_modulus"] = phys_mat.GetRealPropertyValue("YoungsModulus")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
mat_data["poissons_ratio"] = phys_mat.GetRealPropertyValue("PoissonsRatio")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
materials[mat_name] = mat_data
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"material_count": len(materials),
|
||||||
|
"materials": materials,
|
||||||
|
"body_count": len(solid_bodies)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _get_run_journal_exe() -> str:
|
||||||
|
"""Get the path to run_journal.exe."""
|
||||||
|
return os.path.join(NX_BIN_PATH, "run_journal.exe")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_journal(journal_path: str, args: list) -> Tuple[bool, str]:
|
||||||
|
"""Run an NX journal with arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, output_or_error)
|
||||||
|
"""
|
||||||
|
run_journal = _get_run_journal_exe()
|
||||||
|
|
||||||
|
if not os.path.exists(run_journal):
|
||||||
|
return False, f"run_journal.exe not found at {run_journal}"
|
||||||
|
|
||||||
|
cmd = [run_journal, journal_path, "-args"] + args
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120 # 2 minute timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False, f"Journal execution failed: {result.stderr}"
|
||||||
|
|
||||||
|
return True, result.stdout
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Journal execution timed out"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_geometry_operation(
|
||||||
|
operation: str,
|
||||||
|
part_path: str,
|
||||||
|
extra_args: list = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Execute a geometry query operation via NX journal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation: The operation to perform
|
||||||
|
part_path: Path to the part file
|
||||||
|
extra_args: Additional arguments for the operation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with operation result
|
||||||
|
"""
|
||||||
|
# Create temporary journal file
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode='w',
|
||||||
|
suffix='.py',
|
||||||
|
delete=False
|
||||||
|
) as journal_file:
|
||||||
|
journal_file.write(GEOMETRY_QUERY_JOURNAL)
|
||||||
|
journal_path = journal_file.name
|
||||||
|
|
||||||
|
# Create temporary output file
|
||||||
|
output_file = tempfile.NamedTemporaryFile(
|
||||||
|
mode='w',
|
||||||
|
suffix='.json',
|
||||||
|
delete=False
|
||||||
|
).name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build arguments
|
||||||
|
args = [operation, part_path, output_file]
|
||||||
|
if extra_args:
|
||||||
|
args.extend(extra_args)
|
||||||
|
|
||||||
|
# Run the journal
|
||||||
|
success, output = _run_journal(journal_path, args)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
return {"success": False, "error": output, "data": {}}
|
||||||
|
|
||||||
|
# Read the result
|
||||||
|
if os.path.exists(output_file):
|
||||||
|
with open(output_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
else:
|
||||||
|
return {"success": False, "error": "Output file not created", "data": {}}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup temporary files
|
||||||
|
if os.path.exists(journal_path):
|
||||||
|
os.unlink(journal_path)
|
||||||
|
if os.path.exists(output_file):
|
||||||
|
os.unlink(output_file)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Public API
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def get_mass_properties(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Get mass properties from an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with mass, volume, surface_area, centroid,
|
||||||
|
principal_moments, material, density, body_count
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = get_mass_properties("C:/models/bracket.prt")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Mass: {result['data']['mass']} kg")
|
||||||
|
... print(f"Volume: {result['data']['volume']} mm^3")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_geometry_operation("mass_properties", part_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_bodies(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Get information about bodies in an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with count, solid_count, sheet_count, bodies list
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = get_bodies("C:/models/bracket.prt")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Solid bodies: {result['data']['solid_count']}")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_geometry_operation("bodies", part_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_volume(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Get total volume of solid bodies in an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with volume (mm^3), unit, body_count
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = get_volume("C:/models/bracket.prt")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Volume: {result['data']['volume']} mm^3")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_geometry_operation("volume", part_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_surface_area(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Get total surface area of solid bodies in an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with surface_area (mm^2), unit, body_count
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = get_surface_area("C:/models/bracket.prt")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Area: {result['data']['surface_area']} mm^2")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_geometry_operation("surface_area", part_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_material(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Get material information from bodies in an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with material_count, materials dict, body_count
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = get_material("C:/models/bracket.prt")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... for name, mat in result["data"]["materials"].items():
|
||||||
|
... print(f"{name}: density={mat.get('density')}")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_geometry_operation("material", part_path)
|
||||||
1158
optimization_engine/hooks/nx_cad/model_introspection.py
Normal file
1158
optimization_engine/hooks/nx_cad/model_introspection.py
Normal file
File diff suppressed because it is too large
Load Diff
478
optimization_engine/hooks/nx_cad/part_manager.py
Normal file
478
optimization_engine/hooks/nx_cad/part_manager.py
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
"""
|
||||||
|
NX Part Manager Hook
|
||||||
|
====================
|
||||||
|
|
||||||
|
Provides Python functions to open, close, and save NX parts.
|
||||||
|
|
||||||
|
API Reference (verified via Siemens MCP docs):
|
||||||
|
- Session.Parts() -> PartCollection
|
||||||
|
- PartCollection.OpenBase() -> Opens a part file
|
||||||
|
- Part.Close() -> Closes the part
|
||||||
|
- Part.Save() -> Saves the part
|
||||||
|
- Part.SaveAs() -> Saves the part with a new name
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from optimization_engine.hooks.nx_cad import part_manager
|
||||||
|
|
||||||
|
# Open a part
|
||||||
|
part = part_manager.open_part("C:/path/to/part.prt")
|
||||||
|
|
||||||
|
# Save the part
|
||||||
|
part_manager.save_part(part)
|
||||||
|
|
||||||
|
# Close the part
|
||||||
|
part_manager.close_part(part)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any, Tuple
|
||||||
|
|
||||||
|
# NX installation path (configurable)
|
||||||
|
NX_BIN_PATH = os.environ.get(
|
||||||
|
"NX_BIN_PATH",
|
||||||
|
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Journal template for part operations
|
||||||
|
PART_OPERATIONS_JOURNAL = '''
|
||||||
|
# NX Open Python Journal - Part Operations
|
||||||
|
# Auto-generated by Atomizer hooks
|
||||||
|
|
||||||
|
import NXOpen
|
||||||
|
import NXOpen.UF
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Execute part operation based on command arguments."""
|
||||||
|
# Get the NX session
|
||||||
|
session = NXOpen.Session.GetSession()
|
||||||
|
|
||||||
|
# Parse arguments: operation, part_path, [output_json]
|
||||||
|
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
||||||
|
|
||||||
|
if len(args) < 2:
|
||||||
|
raise ValueError("Usage: script.py <operation> <part_path> [output_json]")
|
||||||
|
|
||||||
|
operation = args[0]
|
||||||
|
part_path = args[1]
|
||||||
|
output_json = args[2] if len(args) > 2 else None
|
||||||
|
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if operation == "open":
|
||||||
|
result = open_part(session, part_path)
|
||||||
|
elif operation == "close":
|
||||||
|
result = close_part(session, part_path)
|
||||||
|
elif operation == "save":
|
||||||
|
result = save_part(session, part_path)
|
||||||
|
elif operation == "save_as":
|
||||||
|
new_path = args[3] if len(args) > 3 else None
|
||||||
|
result = save_part_as(session, part_path, new_path)
|
||||||
|
elif operation == "info":
|
||||||
|
result = get_part_info(session, part_path)
|
||||||
|
else:
|
||||||
|
result["error"] = f"Unknown operation: {operation}"
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
# Write result to output JSON if specified
|
||||||
|
if output_json:
|
||||||
|
with open(output_json, 'w') as f:
|
||||||
|
json.dump(result, f, indent=2)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def open_part(session, part_path):
|
||||||
|
"""Open a part file.
|
||||||
|
|
||||||
|
NX Open API: Session.Parts().OpenActiveDisplay()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
result["error"] = f"Part file not found: {part_path}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set load options for the working directory
|
||||||
|
working_dir = os.path.dirname(part_path)
|
||||||
|
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||||
|
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||||
|
|
||||||
|
# Open the part using OpenActiveDisplay (more compatible with batch mode)
|
||||||
|
part, load_status = session.Parts.OpenActiveDisplay(
|
||||||
|
part_path,
|
||||||
|
NXOpen.DisplayPartOption.AllowAdditional
|
||||||
|
)
|
||||||
|
load_status.Dispose()
|
||||||
|
|
||||||
|
if part is None:
|
||||||
|
result["error"] = "Failed to open part - returned None"
|
||||||
|
return result
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"part_name": part.Name,
|
||||||
|
"full_path": part.FullPath,
|
||||||
|
"leaf": part.Leaf,
|
||||||
|
"is_modified": part.IsModified,
|
||||||
|
"is_fully_loaded": part.IsFullyLoaded,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def close_part(session, part_path):
|
||||||
|
"""Close a part.
|
||||||
|
|
||||||
|
NX Open API: Part.Close()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the part in the session
|
||||||
|
part = find_part_by_path(session, part_path)
|
||||||
|
|
||||||
|
if part is None:
|
||||||
|
result["error"] = f"Part not found in session: {part_path}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Close the part
|
||||||
|
# Parameters: close_whole_tree, close_modified, responses
|
||||||
|
part.Close(
|
||||||
|
NXOpen.BasePart.CloseWholeTree.TrueValue,
|
||||||
|
NXOpen.BasePart.CloseModified.CloseModified,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {"closed": part_path}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def save_part(session, part_path):
|
||||||
|
"""Save a part.
|
||||||
|
|
||||||
|
NX Open API: Part.Save()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the part in the session
|
||||||
|
part = find_part_by_path(session, part_path)
|
||||||
|
|
||||||
|
if part is None:
|
||||||
|
result["error"] = f"Part not found in session: {part_path}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Save the part
|
||||||
|
# Parameters: save_component_parts, close_after_save
|
||||||
|
save_status = part.Save(
|
||||||
|
NXOpen.BasePart.SaveComponents.TrueValue,
|
||||||
|
NXOpen.BasePart.CloseAfterSave.FalseValue
|
||||||
|
)
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"saved": part_path,
|
||||||
|
"is_modified": part.IsModified
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def save_part_as(session, part_path, new_path):
|
||||||
|
"""Save a part with a new name.
|
||||||
|
|
||||||
|
NX Open API: Part.SaveAs()
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
if not new_path:
|
||||||
|
result["error"] = "New path is required for SaveAs operation"
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the part in the session
|
||||||
|
part = find_part_by_path(session, part_path)
|
||||||
|
|
||||||
|
if part is None:
|
||||||
|
result["error"] = f"Part not found in session: {part_path}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Save as new file
|
||||||
|
part.SaveAs(new_path)
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"original": part_path,
|
||||||
|
"saved_as": new_path
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_part_info(session, part_path):
|
||||||
|
"""Get information about a part.
|
||||||
|
|
||||||
|
NX Open API: Part properties
|
||||||
|
"""
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the part in the session
|
||||||
|
part = find_part_by_path(session, part_path)
|
||||||
|
|
||||||
|
if part is None:
|
||||||
|
result["error"] = f"Part not found in session: {part_path}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Get part info
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"name": part.Name,
|
||||||
|
"full_path": part.FullPath,
|
||||||
|
"leaf": part.Leaf,
|
||||||
|
"is_modified": part.IsModified,
|
||||||
|
"is_fully_loaded": part.IsFullyLoaded,
|
||||||
|
"is_read_only": part.IsReadOnly,
|
||||||
|
"has_write_access": part.HasWriteAccess,
|
||||||
|
"part_units": str(part.PartUnits),
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def find_part_by_path(session, part_path):
|
||||||
|
"""Find a part in the session by its file path."""
|
||||||
|
part_path_normalized = os.path.normpath(part_path).lower()
|
||||||
|
|
||||||
|
for part in session.Parts:
|
||||||
|
if os.path.normpath(part.FullPath).lower() == part_path_normalized:
|
||||||
|
return part
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _get_run_journal_exe() -> str:
|
||||||
|
"""Get the path to run_journal.exe."""
|
||||||
|
return os.path.join(NX_BIN_PATH, "run_journal.exe")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_journal(journal_path: str, args: list) -> Tuple[bool, str]:
|
||||||
|
"""Run an NX journal with arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, output_or_error)
|
||||||
|
"""
|
||||||
|
run_journal = _get_run_journal_exe()
|
||||||
|
|
||||||
|
if not os.path.exists(run_journal):
|
||||||
|
return False, f"run_journal.exe not found at {run_journal}"
|
||||||
|
|
||||||
|
cmd = [run_journal, journal_path, "-args"] + args
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120 # 2 minute timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False, f"Journal execution failed: {result.stderr}"
|
||||||
|
|
||||||
|
return True, result.stdout
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Journal execution timed out"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_part_operation(
|
||||||
|
operation: str,
|
||||||
|
part_path: str,
|
||||||
|
extra_args: list = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Execute a part operation via NX journal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation: The operation to perform (open, close, save, save_as, info)
|
||||||
|
part_path: Path to the part file
|
||||||
|
extra_args: Additional arguments for the operation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with operation result
|
||||||
|
"""
|
||||||
|
# Create temporary journal file
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode='w',
|
||||||
|
suffix='.py',
|
||||||
|
delete=False
|
||||||
|
) as journal_file:
|
||||||
|
journal_file.write(PART_OPERATIONS_JOURNAL)
|
||||||
|
journal_path = journal_file.name
|
||||||
|
|
||||||
|
# Create temporary output file
|
||||||
|
output_file = tempfile.NamedTemporaryFile(
|
||||||
|
mode='w',
|
||||||
|
suffix='.json',
|
||||||
|
delete=False
|
||||||
|
).name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build arguments
|
||||||
|
args = [operation, part_path, output_file]
|
||||||
|
if extra_args:
|
||||||
|
args.extend(extra_args)
|
||||||
|
|
||||||
|
# Run the journal
|
||||||
|
success, output = _run_journal(journal_path, args)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
return {"success": False, "error": output, "data": {}}
|
||||||
|
|
||||||
|
# Read the result
|
||||||
|
if os.path.exists(output_file):
|
||||||
|
with open(output_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
else:
|
||||||
|
return {"success": False, "error": "Output file not created", "data": {}}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup temporary files
|
||||||
|
if os.path.exists(journal_path):
|
||||||
|
os.unlink(journal_path)
|
||||||
|
if os.path.exists(output_file):
|
||||||
|
os.unlink(output_file)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Public API
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def open_part(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Open an NX part file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with part_name, full_path, leaf, is_modified, is_fully_loaded
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = open_part("C:/models/bracket.prt")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"Opened: {result['data']['part_name']}")
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Part file not found: {part_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _execute_part_operation("open", part_path)
|
||||||
|
|
||||||
|
|
||||||
|
def close_part(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Close an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with closed path
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
return _execute_part_operation("close", part_path)
|
||||||
|
|
||||||
|
|
||||||
|
def save_part(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Save an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with saved path and is_modified flag
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
return _execute_part_operation("save", part_path)
|
||||||
|
|
||||||
|
|
||||||
|
def save_part_as(part_path: str, new_path: str) -> Dict[str, Any]:
|
||||||
|
"""Save an NX part with a new name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the original .prt file
|
||||||
|
new_path: Full path for the new file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with original and saved_as paths
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
new_path = os.path.abspath(new_path)
|
||||||
|
return _execute_part_operation("save_as", part_path, [new_path])
|
||||||
|
|
||||||
|
|
||||||
|
def get_part_info(part_path: str) -> Dict[str, Any]:
|
||||||
|
"""Get information about an NX part.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
part_path: Full path to the .prt file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- success: bool
|
||||||
|
- error: Optional error message
|
||||||
|
- data: Dict with name, full_path, leaf, is_modified,
|
||||||
|
is_fully_loaded, is_read_only, has_write_access, part_units
|
||||||
|
"""
|
||||||
|
part_path = os.path.abspath(part_path)
|
||||||
|
return _execute_part_operation("info", part_path)
|
||||||
18
optimization_engine/hooks/nx_cae/__init__.py
Normal file
18
optimization_engine/hooks/nx_cae/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
NX CAE Hooks
|
||||||
|
============
|
||||||
|
|
||||||
|
Python hooks for NX CAE (FEM/Simulation) operations via NX Open API.
|
||||||
|
|
||||||
|
Modules
|
||||||
|
-------
|
||||||
|
solver_manager : Solution export and solve operations
|
||||||
|
- export_bdf: Export Nastran deck without solving
|
||||||
|
- solve_simulation: Solve a simulation solution
|
||||||
|
|
||||||
|
Phase 2 Task 2.1 - NX Open Automation Roadmap
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import solver_manager
|
||||||
|
|
||||||
|
__all__ = ['solver_manager']
|
||||||
472
optimization_engine/hooks/nx_cae/solver_manager.py
Normal file
472
optimization_engine/hooks/nx_cae/solver_manager.py
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
"""
|
||||||
|
NX Solver Manager Hook
|
||||||
|
======================
|
||||||
|
|
||||||
|
Provides Python functions to export BDF decks and solve simulations.
|
||||||
|
|
||||||
|
API Reference (NX Open):
|
||||||
|
- SimSolution.ExportSolver() -> Export Nastran deck (.dat/.bdf)
|
||||||
|
- SimSolution.Solve() -> Solve a single solution
|
||||||
|
- SimSolveManager.SolveChainOfSolutions() -> Solve solution chain
|
||||||
|
|
||||||
|
Phase 2 Task 2.1 - NX Open Automation Roadmap
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from optimization_engine.hooks.nx_cae import solver_manager
|
||||||
|
|
||||||
|
# Export BDF without solving
|
||||||
|
result = solver_manager.export_bdf(
|
||||||
|
"C:/model.sim",
|
||||||
|
"Solution 1",
|
||||||
|
"C:/output/model.dat"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Solve simulation
|
||||||
|
result = solver_manager.solve_simulation("C:/model.sim", "Solution 1")
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
# NX installation path (configurable)
|
||||||
|
NX_BIN_PATH = os.environ.get(
|
||||||
|
"NX_BIN_PATH",
|
||||||
|
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Journal template for BDF export
|
||||||
|
BDF_EXPORT_JOURNAL = '''
|
||||||
|
# NX Open Python Journal - BDF Export
|
||||||
|
# Auto-generated by Atomizer hooks
|
||||||
|
# Phase 2 Task 2.1 - NX Open Automation Roadmap
|
||||||
|
|
||||||
|
import NXOpen
|
||||||
|
import NXOpen.CAE
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Export BDF/DAT file from a simulation solution."""
|
||||||
|
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
||||||
|
|
||||||
|
if len(args) < 3:
|
||||||
|
raise ValueError("Usage: script.py <sim_path> <solution_name> <output_bdf> [output_json]")
|
||||||
|
|
||||||
|
sim_path = args[0]
|
||||||
|
solution_name = args[1]
|
||||||
|
output_bdf = args[2]
|
||||||
|
output_json = args[3] if len(args) > 3 else None
|
||||||
|
|
||||||
|
result = {"success": False, "error": None, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
session = NXOpen.Session.GetSession()
|
||||||
|
|
||||||
|
# Set load options
|
||||||
|
working_dir = os.path.dirname(sim_path)
|
||||||
|
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||||
|
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||||
|
|
||||||
|
# Open the simulation file
|
||||||
|
print(f"[JOURNAL] Opening simulation: {sim_path}")
|
||||||
|
basePart, loadStatus = session.Parts.OpenActiveDisplay(
|
||||||
|
sim_path,
|
||||||
|
NXOpen.DisplayPartOption.AllowAdditional
|
||||||
|
)
|
||||||
|
loadStatus.Dispose()
|
||||||
|
|
||||||
|
# Get the sim part
|
||||||
|
simPart = session.Parts.Work
|
||||||
|
if not isinstance(simPart, NXOpen.CAE.SimPart):
|
||||||
|
raise ValueError(f"Part is not a SimPart: {type(simPart)}")
|
||||||
|
|
||||||
|
simSimulation = simPart.Simulation
|
||||||
|
print(f"[JOURNAL] Simulation: {simSimulation.Name}")
|
||||||
|
|
||||||
|
# Find the solution
|
||||||
|
solution = None
|
||||||
|
for sol in simSimulation.Solutions:
|
||||||
|
if sol.Name == solution_name:
|
||||||
|
solution = sol
|
||||||
|
break
|
||||||
|
|
||||||
|
if solution is None:
|
||||||
|
# Try to find by index or use first solution
|
||||||
|
solutions = list(simSimulation.Solutions)
|
||||||
|
if solutions:
|
||||||
|
solution = solutions[0]
|
||||||
|
print(f"[JOURNAL] Solution '{solution_name}' not found, using '{solution.Name}'")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"No solutions found in simulation")
|
||||||
|
|
||||||
|
print(f"[JOURNAL] Solution: {solution.Name}")
|
||||||
|
|
||||||
|
# Export the solver deck
|
||||||
|
# The ExportSolver method exports the Nastran input deck
|
||||||
|
print(f"[JOURNAL] Exporting BDF to: {output_bdf}")
|
||||||
|
|
||||||
|
# Create export builder
|
||||||
|
# NX API: SimSolution has methods for exporting
|
||||||
|
# Method 1: Try ExportSolver if available
|
||||||
|
try:
|
||||||
|
# Some NX versions use NastranSolverExportBuilder
|
||||||
|
exportBuilder = solution.CreateNastranSolverExportBuilder()
|
||||||
|
exportBuilder.NastranInputFile = output_bdf
|
||||||
|
exportBuilder.Commit()
|
||||||
|
exportBuilder.Destroy()
|
||||||
|
print("[JOURNAL] Exported via NastranSolverExportBuilder")
|
||||||
|
except AttributeError:
|
||||||
|
# Method 2: Alternative - solve and copy output
|
||||||
|
# When solving, NX creates the deck in SXXXXX folder
|
||||||
|
print("[JOURNAL] NastranSolverExportBuilder not available")
|
||||||
|
print("[JOURNAL] BDF export requires solving - use solve_simulation instead")
|
||||||
|
raise ValueError("Direct BDF export not available in this NX version. "
|
||||||
|
"Use solve_simulation() and find BDF in solution folder.")
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["data"] = {
|
||||||
|
"output_file": output_bdf,
|
||||||
|
"solution_name": solution.Name,
|
||||||
|
"simulation": simSimulation.Name,
|
||||||
|
}
|
||||||
|
print(f"[JOURNAL] Export completed successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = str(e)
|
||||||
|
print(f"[JOURNAL] ERROR: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Write result
|
||||||
|
if output_json:
|
||||||
|
with open(output_json, 'w') as f:
|
||||||
|
json.dump(result, f, indent=2)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _run_journal(journal_content: str, *args) -> Dict[str, Any]:
|
||||||
|
"""Execute an NX journal script and return the result."""
|
||||||
|
run_journal_exe = Path(NX_BIN_PATH) / "run_journal.exe"
|
||||||
|
if not run_journal_exe.exists():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"run_journal.exe not found at {run_journal_exe}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create temporary files
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as journal_file:
|
||||||
|
journal_file.write(journal_content)
|
||||||
|
journal_path = journal_file.name
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as output_file:
|
||||||
|
output_path = output_file.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build command
|
||||||
|
cmd = [str(run_journal_exe), journal_path, "-args"]
|
||||||
|
cmd.extend(str(a) for a in args)
|
||||||
|
cmd.append(output_path)
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120 # 2 minute timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read result
|
||||||
|
if os.path.exists(output_path):
|
||||||
|
with open(output_path, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"No output file generated. stdout: {result.stdout}, stderr: {result.stderr}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Journal execution timed out after 120 seconds",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
try:
|
||||||
|
os.unlink(journal_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.unlink(output_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def export_bdf(
|
||||||
|
sim_path: str,
|
||||||
|
solution_name: str = "Solution 1",
|
||||||
|
output_bdf: Optional[str] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Export Nastran deck (BDF/DAT) from a simulation without solving.
|
||||||
|
|
||||||
|
Note: This functionality depends on NX version. Some versions require
|
||||||
|
solving to generate the BDF. Use solve_simulation() and locate the BDF
|
||||||
|
in the solution folder (SXXXXX/*.dat) as an alternative.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sim_path: Path to .sim file
|
||||||
|
solution_name: Name of solution to export (default "Solution 1")
|
||||||
|
output_bdf: Output path for BDF file (default: same dir as sim)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {
|
||||||
|
'success': bool,
|
||||||
|
'error': str or None,
|
||||||
|
'data': {
|
||||||
|
'output_file': Path to exported BDF,
|
||||||
|
'solution_name': Solution name used,
|
||||||
|
'simulation': Simulation name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = export_bdf("C:/model.sim", "Solution 1", "C:/output/model.dat")
|
||||||
|
>>> if result["success"]:
|
||||||
|
... print(f"BDF exported to: {result['data']['output_file']}")
|
||||||
|
"""
|
||||||
|
sim_path = str(Path(sim_path).resolve())
|
||||||
|
|
||||||
|
if not Path(sim_path).exists():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Simulation file not found: {sim_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if output_bdf is None:
|
||||||
|
sim_dir = Path(sim_path).parent
|
||||||
|
sim_name = Path(sim_path).stem
|
||||||
|
output_bdf = str(sim_dir / f"{sim_name}.dat")
|
||||||
|
|
||||||
|
return _run_journal(BDF_EXPORT_JOURNAL, sim_path, solution_name, output_bdf)
|
||||||
|
|
||||||
|
|
||||||
|
def get_bdf_from_solution_folder(
|
||||||
|
sim_path: str,
|
||||||
|
solution_name: str = "Solution 1"
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Locate BDF file in the solution output folder.
|
||||||
|
|
||||||
|
After solving, NX creates a folder structure like:
|
||||||
|
- model_sim1_fem1_SXXXXX/
|
||||||
|
- model_sim1_fem1.dat (BDF file)
|
||||||
|
- model_sim1_fem1.op2 (results)
|
||||||
|
|
||||||
|
This function finds the BDF without running export.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sim_path: Path to .sim file
|
||||||
|
solution_name: Name of solution
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {
|
||||||
|
'success': bool,
|
||||||
|
'error': str or None,
|
||||||
|
'data': {
|
||||||
|
'bdf_file': Path to BDF if found,
|
||||||
|
'solution_folders': List of found solution folders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
sim_path = Path(sim_path)
|
||||||
|
if not sim_path.exists():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Simulation file not found: {sim_path}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
sim_dir = sim_path.parent
|
||||||
|
sim_stem = sim_path.stem
|
||||||
|
|
||||||
|
# Search for solution folders (pattern: *_SXXXXX)
|
||||||
|
solution_folders = list(sim_dir.glob(f"{sim_stem}*_S[0-9]*"))
|
||||||
|
|
||||||
|
if not solution_folders:
|
||||||
|
# Also try simpler patterns
|
||||||
|
solution_folders = list(sim_dir.glob("*_S[0-9]*"))
|
||||||
|
|
||||||
|
bdf_files = []
|
||||||
|
for folder in solution_folders:
|
||||||
|
if folder.is_dir():
|
||||||
|
# Look for .dat or .bdf files
|
||||||
|
dat_files = list(folder.glob("*.dat"))
|
||||||
|
bdf_files.extend(dat_files)
|
||||||
|
|
||||||
|
if bdf_files:
|
||||||
|
# Return the most recent one
|
||||||
|
bdf_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"error": None,
|
||||||
|
"data": {
|
||||||
|
"bdf_file": str(bdf_files[0]),
|
||||||
|
"all_bdf_files": [str(f) for f in bdf_files],
|
||||||
|
"solution_folders": [str(f) for f in solution_folders]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "No BDF files found. Ensure the simulation has been solved.",
|
||||||
|
"data": {
|
||||||
|
"solution_folders": [str(f) for f in solution_folders]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def solve_simulation(
|
||||||
|
sim_path: str,
|
||||||
|
solution_name: str = "Solution 1",
|
||||||
|
expression_updates: Optional[Dict[str, float]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Solve a simulation solution.
|
||||||
|
|
||||||
|
This uses the existing solve_simulation.py journal which handles both
|
||||||
|
single-part and assembly FEM workflows.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sim_path: Path to .sim file
|
||||||
|
solution_name: Name of solution to solve (default "Solution 1")
|
||||||
|
expression_updates: Optional dict of {expression_name: value} to update
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {
|
||||||
|
'success': bool,
|
||||||
|
'error': str or None,
|
||||||
|
'data': {
|
||||||
|
'solution_folder': Path to solution output folder,
|
||||||
|
'op2_file': Path to OP2 results file,
|
||||||
|
'bdf_file': Path to BDF input file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Note:
|
||||||
|
For full solve functionality, use the NXSolver class in
|
||||||
|
optimization_engine/nx_solver.py which provides more features
|
||||||
|
like iteration folders and batch processing.
|
||||||
|
"""
|
||||||
|
# This is a simplified wrapper - for full functionality use NXSolver
|
||||||
|
solve_journal = Path(__file__).parent.parent.parent / "solve_simulation.py"
|
||||||
|
|
||||||
|
if not solve_journal.exists():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Solve journal not found: {solve_journal}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
run_journal_exe = Path(NX_BIN_PATH) / "run_journal.exe"
|
||||||
|
if not run_journal_exe.exists():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"run_journal.exe not found at {run_journal_exe}",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build command
|
||||||
|
cmd = [str(run_journal_exe), str(solve_journal), "-args", sim_path, solution_name]
|
||||||
|
|
||||||
|
# Add expression updates
|
||||||
|
if expression_updates:
|
||||||
|
for name, value in expression_updates.items():
|
||||||
|
cmd.append(f"{name}={value}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=600 # 10 minute timeout for solving
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for success in output
|
||||||
|
if "Solve completed successfully" in result.stdout or result.returncode == 0:
|
||||||
|
# Find output files
|
||||||
|
bdf_result = get_bdf_from_solution_folder(sim_path, solution_name)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"error": None,
|
||||||
|
"data": {
|
||||||
|
"stdout": result.stdout[-2000:], # Last 2000 chars
|
||||||
|
"bdf_file": bdf_result["data"].get("bdf_file") if bdf_result["success"] else None,
|
||||||
|
"solution_folders": bdf_result["data"].get("solution_folders", [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Solve may have failed. Check output.",
|
||||||
|
"data": {
|
||||||
|
"stdout": result.stdout[-2000:],
|
||||||
|
"stderr": result.stderr[-1000:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Solve timed out after 600 seconds",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Example usage
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
sim_path = sys.argv[1]
|
||||||
|
solution = sys.argv[2] if len(sys.argv) > 2 else "Solution 1"
|
||||||
|
|
||||||
|
print(f"Looking for BDF in solution folder...")
|
||||||
|
result = get_bdf_from_solution_folder(sim_path, solution)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
print(f"Found BDF: {result['data']['bdf_file']}")
|
||||||
|
else:
|
||||||
|
print(f"Error: {result['error']}")
|
||||||
|
print(f"Trying to export...")
|
||||||
|
result = export_bdf(sim_path, solution)
|
||||||
|
print(f"Export result: {result}")
|
||||||
|
else:
|
||||||
|
print("Usage: python solver_manager.py <sim_path> [solution_name]")
|
||||||
125
optimization_engine/hooks/test_hooks.py
Normal file
125
optimization_engine/hooks/test_hooks.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
Test script for NX Open hooks.
|
||||||
|
|
||||||
|
This script tests the hooks module with a real NX part.
|
||||||
|
Run with: python -m optimization_engine.hooks.test_hooks
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Add the project root to path
|
||||||
|
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
sys.path.insert(0, project_root)
|
||||||
|
|
||||||
|
from optimization_engine.hooks.nx_cad import (
|
||||||
|
part_manager,
|
||||||
|
expression_manager,
|
||||||
|
geometry_query,
|
||||||
|
feature_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hooks(part_path: str):
|
||||||
|
"""Test all hooks with the given part."""
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"Testing NX Open Hooks")
|
||||||
|
print(f"Part: {part_path}")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
if not os.path.exists(part_path):
|
||||||
|
print(f"ERROR: Part file not found: {part_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
|
||||||
|
# Test 1: Get expressions
|
||||||
|
print("\n--- Test 1: Get Expressions ---")
|
||||||
|
result = expression_manager.get_expressions(part_path)
|
||||||
|
if result["success"]:
|
||||||
|
print(f"SUCCESS: Found {result['data']['count']} expressions")
|
||||||
|
# Show first 5 expressions
|
||||||
|
for i, (name, expr) in enumerate(list(result['data']['expressions'].items())[:5]):
|
||||||
|
print(f" {name} = {expr['value']} {expr.get('units', '')}")
|
||||||
|
if result['data']['count'] > 5:
|
||||||
|
print(f" ... and {result['data']['count'] - 5} more")
|
||||||
|
else:
|
||||||
|
print(f"FAILED: {result['error']}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Test 2: Get mass properties
|
||||||
|
print("\n--- Test 2: Get Mass Properties ---")
|
||||||
|
result = geometry_query.get_mass_properties(part_path)
|
||||||
|
if result["success"]:
|
||||||
|
data = result['data']
|
||||||
|
print(f"SUCCESS:")
|
||||||
|
print(f" Mass: {data['mass']:.6f} {data['mass_unit']}")
|
||||||
|
print(f" Volume: {data['volume']:.2f} {data['volume_unit']}")
|
||||||
|
print(f" Surface Area: {data['surface_area']:.2f} {data['area_unit']}")
|
||||||
|
print(f" Material: {data.get('material', 'N/A')}")
|
||||||
|
print(f" Centroid: ({data['centroid']['x']:.2f}, {data['centroid']['y']:.2f}, {data['centroid']['z']:.2f}) mm")
|
||||||
|
else:
|
||||||
|
print(f"FAILED: {result['error']}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Test 3: Get bodies
|
||||||
|
print("\n--- Test 3: Get Bodies ---")
|
||||||
|
result = geometry_query.get_bodies(part_path)
|
||||||
|
if result["success"]:
|
||||||
|
data = result['data']
|
||||||
|
print(f"SUCCESS:")
|
||||||
|
print(f" Total bodies: {data['count']}")
|
||||||
|
print(f" Solid bodies: {data['solid_count']}")
|
||||||
|
print(f" Sheet bodies: {data['sheet_count']}")
|
||||||
|
else:
|
||||||
|
print(f"FAILED: {result['error']}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Test 4: Get features
|
||||||
|
print("\n--- Test 4: Get Features ---")
|
||||||
|
result = feature_manager.get_features(part_path)
|
||||||
|
if result["success"]:
|
||||||
|
data = result['data']
|
||||||
|
print(f"SUCCESS: Found {data['count']} features ({data['suppressed_count']} suppressed)")
|
||||||
|
# Show first 5 features
|
||||||
|
for i, feat in enumerate(data['features'][:5]):
|
||||||
|
status = "suppressed" if feat['is_suppressed'] else "active"
|
||||||
|
print(f" {feat['name']} ({feat['type']}): {status}")
|
||||||
|
if data['count'] > 5:
|
||||||
|
print(f" ... and {data['count'] - 5} more")
|
||||||
|
else:
|
||||||
|
print(f"FAILED: {result['error']}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
if all_passed:
|
||||||
|
print("ALL TESTS PASSED!")
|
||||||
|
else:
|
||||||
|
print("SOME TESTS FAILED")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
return all_passed
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
# Default to bracket study part
|
||||||
|
default_part = os.path.join(
|
||||||
|
project_root,
|
||||||
|
"studies/bracket_stiffness_optimization_V3/1_setup/model/Bracket.prt"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use command line argument if provided
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
part_path = sys.argv[1]
|
||||||
|
else:
|
||||||
|
part_path = default_part
|
||||||
|
|
||||||
|
success = test_hooks(part_path)
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
121
optimization_engine/hooks/test_introspection.py
Normal file
121
optimization_engine/hooks/test_introspection.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"""Test model introspection module."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import glob
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
|
from optimization_engine.hooks.nx_cad.model_introspection import (
|
||||||
|
introspect_op2,
|
||||||
|
introspect_study,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_op2_introspection():
|
||||||
|
"""Test OP2 introspection on bracket study."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("OP2 INTROSPECTION TEST")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Find bracket OP2 files
|
||||||
|
op2_files = glob.glob(
|
||||||
|
"C:/Users/Antoine/Atomizer/studies/bracket_stiffness_optimization_V3/**/*.op2",
|
||||||
|
recursive=True
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\nFound {len(op2_files)} OP2 files")
|
||||||
|
for f in op2_files[:5]:
|
||||||
|
print(f" - {Path(f).name}")
|
||||||
|
|
||||||
|
if not op2_files:
|
||||||
|
print("No OP2 files found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Introspect first OP2
|
||||||
|
print(f"\nIntrospecting: {Path(op2_files[0]).name}")
|
||||||
|
result = introspect_op2(op2_files[0])
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
print(f"ERROR: {result['error']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = result["data"]
|
||||||
|
|
||||||
|
# Print results
|
||||||
|
print(f"\nFile Info:")
|
||||||
|
print(f" Size: {data['file_info']['size_mb']:.2f} MB")
|
||||||
|
print(f" Subcases: {data['subcases']}")
|
||||||
|
|
||||||
|
print(f"\nAvailable Results:")
|
||||||
|
for r_type, info in data["results"].items():
|
||||||
|
status = "YES" if info["available"] else "no"
|
||||||
|
extra = ""
|
||||||
|
if info["available"]:
|
||||||
|
if "element_types" in info and info["element_types"]:
|
||||||
|
extra = f" ({', '.join(info['element_types'][:3])})"
|
||||||
|
elif "subcases" in info and info["subcases"]:
|
||||||
|
extra = f" (subcases: {info['subcases'][:3]})"
|
||||||
|
print(f" {r_type:20s}: {status:4s} {extra}")
|
||||||
|
|
||||||
|
print(f"\nMesh Info:")
|
||||||
|
print(f" Nodes: {data['mesh']['node_count']}")
|
||||||
|
print(f" Elements: {data['mesh']['element_count']}")
|
||||||
|
if data['mesh']['element_types']:
|
||||||
|
print(f" Element types: {list(data['mesh']['element_types'].keys())[:5]}")
|
||||||
|
|
||||||
|
print(f"\nExtractable results: {data['extractable']}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_study_introspection():
|
||||||
|
"""Test study directory introspection."""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("STUDY INTROSPECTION TEST")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
study_dir = "C:/Users/Antoine/Atomizer/studies/bracket_stiffness_optimization_V3"
|
||||||
|
print(f"\nIntrospecting study: {study_dir}")
|
||||||
|
|
||||||
|
result = introspect_study(study_dir)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
print(f"ERROR: {result['error']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = result["data"]
|
||||||
|
|
||||||
|
print(f"\nStudy Summary:")
|
||||||
|
print(f" Parts (.prt): {data['summary']['part_count']}")
|
||||||
|
print(f" Simulations (.sim): {data['summary']['simulation_count']}")
|
||||||
|
print(f" Results (.op2): {data['summary']['results_count']}")
|
||||||
|
print(f" Has config: {data['summary']['has_config']}")
|
||||||
|
|
||||||
|
print(f"\nParts found:")
|
||||||
|
for p in data["parts"][:5]:
|
||||||
|
print(f" - {Path(p['path']).name}")
|
||||||
|
|
||||||
|
print(f"\nSimulations found:")
|
||||||
|
for s in data["simulations"][:5]:
|
||||||
|
print(f" - {Path(s['path']).name}")
|
||||||
|
|
||||||
|
if data["config"]:
|
||||||
|
print(f"\nOptimization Config:")
|
||||||
|
config = data["config"]
|
||||||
|
if "variables" in config:
|
||||||
|
print(f" Variables: {len(config['variables'])}")
|
||||||
|
for v in config["variables"][:3]:
|
||||||
|
print(f" - {v.get('name', 'unnamed')}: [{v.get('lower')}, {v.get('upper')}]")
|
||||||
|
if "objectives" in config:
|
||||||
|
print(f" Objectives: {len(config['objectives'])}")
|
||||||
|
for o in config["objectives"][:3]:
|
||||||
|
print(f" - {o.get('name', 'unnamed')} ({o.get('direction', 'minimize')})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_op2_introspection()
|
||||||
|
test_study_introspection()
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("INTROSPECTION TESTS COMPLETE")
|
||||||
|
print("=" * 60)
|
||||||
@@ -676,7 +676,13 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
|||||||
|
|
||||||
def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_updates, working_dir):
|
def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_updates, working_dir):
|
||||||
"""
|
"""
|
||||||
Simple workflow for single-part simulations or when no expression updates needed.
|
Workflow for single-part simulations with optional expression updates.
|
||||||
|
|
||||||
|
For single-part FEMs (Bracket.prt -> Bracket_fem1.fem -> Bracket_sim1.sim):
|
||||||
|
1. Open the .sim file (this loads .fem and .prt)
|
||||||
|
2. If expression_updates: find the geometry .prt, update expressions, rebuild
|
||||||
|
3. Update the FEM mesh
|
||||||
|
4. Solve
|
||||||
"""
|
"""
|
||||||
print(f"[JOURNAL] Opening simulation: {sim_file_path}")
|
print(f"[JOURNAL] Opening simulation: {sim_file_path}")
|
||||||
|
|
||||||
@@ -688,6 +694,192 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
|||||||
partLoadStatus1.Dispose()
|
partLoadStatus1.Dispose()
|
||||||
|
|
||||||
workSimPart = theSession.Parts.BaseWork
|
workSimPart = theSession.Parts.BaseWork
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# STEP 1: UPDATE EXPRESSIONS IN GEOMETRY PART (if any)
|
||||||
|
# =========================================================================
|
||||||
|
if expression_updates:
|
||||||
|
print(f"[JOURNAL] STEP 1: Updating expressions in geometry part...")
|
||||||
|
|
||||||
|
# List all loaded parts for debugging
|
||||||
|
print(f"[JOURNAL] Currently loaded parts:")
|
||||||
|
for part in theSession.Parts:
|
||||||
|
print(f"[JOURNAL] - {part.Name} (type: {type(part).__name__})")
|
||||||
|
|
||||||
|
# NX doesn't automatically load the geometry .prt when opening a SIM file
|
||||||
|
# We need to find and load it explicitly from the working directory
|
||||||
|
geom_part = None
|
||||||
|
|
||||||
|
# First, try to find an already loaded geometry part
|
||||||
|
for part in theSession.Parts:
|
||||||
|
part_name = part.Name.lower()
|
||||||
|
part_type = type(part).__name__
|
||||||
|
|
||||||
|
# Skip FEM and SIM parts by type
|
||||||
|
if 'fem' in part_type.lower() or 'sim' in part_type.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip parts with _fem or _sim in name
|
||||||
|
if '_fem' in part_name or '_sim' in part_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
geom_part = part
|
||||||
|
print(f"[JOURNAL] Found geometry part (already loaded): {part.Name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# If not found, try to load the geometry .prt file from working directory
|
||||||
|
if geom_part is None:
|
||||||
|
print(f"[JOURNAL] Geometry part not loaded, searching for .prt file...")
|
||||||
|
for filename in os.listdir(working_dir):
|
||||||
|
if filename.endswith('.prt') and '_fem' not in filename.lower() and '_sim' not in filename.lower():
|
||||||
|
prt_path = os.path.join(working_dir, filename)
|
||||||
|
print(f"[JOURNAL] Loading geometry part: {filename}")
|
||||||
|
try:
|
||||||
|
geom_part, partLoadStatus = theSession.Parts.Open(prt_path)
|
||||||
|
partLoadStatus.Dispose()
|
||||||
|
print(f"[JOURNAL] Geometry part loaded: {geom_part.Name}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[JOURNAL] WARNING: Could not load {filename}: {e}")
|
||||||
|
|
||||||
|
if geom_part:
|
||||||
|
try:
|
||||||
|
# Switch to the geometry part for expression editing
|
||||||
|
markId_expr = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Update Expressions")
|
||||||
|
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||||
|
geom_part,
|
||||||
|
NXOpen.DisplayPartOption.AllowAdditional,
|
||||||
|
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||||
|
)
|
||||||
|
partLoadStatus.Dispose()
|
||||||
|
|
||||||
|
# Switch to modeling application for expression editing
|
||||||
|
theSession.ApplicationSwitchImmediate("UG_APP_MODELING")
|
||||||
|
|
||||||
|
workPart = theSession.Parts.Work
|
||||||
|
|
||||||
|
# Write expressions to temp file and import
|
||||||
|
exp_file_path = os.path.join(working_dir, "_temp_expressions.exp")
|
||||||
|
with open(exp_file_path, 'w') as f:
|
||||||
|
for expr_name, expr_value in expression_updates.items():
|
||||||
|
# Determine unit based on name
|
||||||
|
if 'angle' in expr_name.lower():
|
||||||
|
unit_str = "Degrees"
|
||||||
|
else:
|
||||||
|
unit_str = "MilliMeter"
|
||||||
|
f.write(f"[{unit_str}]{expr_name}={expr_value}\n")
|
||||||
|
print(f"[JOURNAL] {expr_name} = {expr_value} ({unit_str})")
|
||||||
|
|
||||||
|
print(f"[JOURNAL] Importing expressions...")
|
||||||
|
expModified, errorMessages = workPart.Expressions.ImportFromFile(
|
||||||
|
exp_file_path,
|
||||||
|
NXOpen.ExpressionCollection.ImportMode.Replace
|
||||||
|
)
|
||||||
|
print(f"[JOURNAL] Expressions modified: {expModified}")
|
||||||
|
if errorMessages:
|
||||||
|
print(f"[JOURNAL] Import messages: {errorMessages}")
|
||||||
|
|
||||||
|
# Update geometry
|
||||||
|
print(f"[JOURNAL] Rebuilding geometry...")
|
||||||
|
markId_update = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "NX update")
|
||||||
|
nErrs = theSession.UpdateManager.DoUpdate(markId_update)
|
||||||
|
theSession.DeleteUndoMark(markId_update, "NX update")
|
||||||
|
print(f"[JOURNAL] Geometry rebuilt ({nErrs} errors)")
|
||||||
|
|
||||||
|
# Save geometry part
|
||||||
|
print(f"[JOURNAL] Saving geometry part...")
|
||||||
|
partSaveStatus_geom = workPart.Save(NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue)
|
||||||
|
partSaveStatus_geom.Dispose()
|
||||||
|
|
||||||
|
# Clean up temp file
|
||||||
|
try:
|
||||||
|
os.remove(exp_file_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[JOURNAL] ERROR updating expressions: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
else:
|
||||||
|
print(f"[JOURNAL] WARNING: Could not find geometry part for expression updates!")
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# STEP 2: UPDATE FEM MESH (if expressions were updated)
|
||||||
|
# =========================================================================
|
||||||
|
if expression_updates:
|
||||||
|
print(f"[JOURNAL] STEP 2: Updating FEM mesh...")
|
||||||
|
|
||||||
|
# First, load the idealized part if it exists (required for mesh update chain)
|
||||||
|
# The chain is: .prt (geometry) -> _i.prt (idealized) -> .fem (mesh)
|
||||||
|
idealized_part = None
|
||||||
|
for filename in os.listdir(working_dir):
|
||||||
|
if '_i.prt' in filename.lower():
|
||||||
|
idealized_path = os.path.join(working_dir, filename)
|
||||||
|
print(f"[JOURNAL] Loading idealized part: {filename}")
|
||||||
|
try:
|
||||||
|
idealized_part, partLoadStatus = theSession.Parts.Open(idealized_path)
|
||||||
|
partLoadStatus.Dispose()
|
||||||
|
print(f"[JOURNAL] Idealized part loaded: {idealized_part.Name}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[JOURNAL] WARNING: Could not load idealized part: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Find the FEM part
|
||||||
|
fem_part = None
|
||||||
|
for part in theSession.Parts:
|
||||||
|
if '_fem' in part.Name.lower() or part.Name.lower().endswith('.fem'):
|
||||||
|
fem_part = part
|
||||||
|
print(f"[JOURNAL] Found FEM part: {part.Name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if fem_part:
|
||||||
|
try:
|
||||||
|
# Switch to FEM part - CRITICAL: Use SameAsDisplay to make FEM the work part
|
||||||
|
# This is required for UpdateFemodel() to properly regenerate the mesh
|
||||||
|
# Reference: tests/journal_with_regenerate.py line 76
|
||||||
|
print(f"[JOURNAL] Switching to FEM part: {fem_part.Name}")
|
||||||
|
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||||
|
fem_part,
|
||||||
|
NXOpen.DisplayPartOption.AllowAdditional,
|
||||||
|
NXOpen.PartDisplayPartWorkPartOption.SameAsDisplay # Critical fix!
|
||||||
|
)
|
||||||
|
partLoadStatus.Dispose()
|
||||||
|
|
||||||
|
# Switch to FEM application
|
||||||
|
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
|
||||||
|
|
||||||
|
# Update the FE model
|
||||||
|
workFemPart = theSession.Parts.BaseWork
|
||||||
|
feModel = workFemPart.FindObject("FEModel")
|
||||||
|
|
||||||
|
print(f"[JOURNAL] Updating FE model...")
|
||||||
|
feModel.UpdateFemodel()
|
||||||
|
print(f"[JOURNAL] FE model updated")
|
||||||
|
|
||||||
|
# Save FEM
|
||||||
|
partSaveStatus_fem = workFemPart.Save(NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue)
|
||||||
|
partSaveStatus_fem.Dispose()
|
||||||
|
print(f"[JOURNAL] FEM saved")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[JOURNAL] ERROR updating FEM: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# STEP 3: SWITCH BACK TO SIM AND SOLVE
|
||||||
|
# =========================================================================
|
||||||
|
print(f"[JOURNAL] STEP 3: Solving simulation...")
|
||||||
|
|
||||||
|
# Switch back to sim part
|
||||||
|
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||||
|
workSimPart,
|
||||||
|
NXOpen.DisplayPartOption.AllowAdditional,
|
||||||
|
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||||
|
)
|
||||||
|
partLoadStatus.Dispose()
|
||||||
|
|
||||||
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
|
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
|
||||||
theSession.Post.UpdateUserGroupsFromSimPart(workSimPart)
|
theSession.Post.UpdateUserGroupsFromSimPart(workSimPart)
|
||||||
|
|
||||||
@@ -710,7 +902,7 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
|||||||
psolutions1,
|
psolutions1,
|
||||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
|
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
|
||||||
NXOpen.CAE.SimSolution.SolveMode.Background
|
NXOpen.CAE.SimSolution.SolveMode.Foreground # Use Foreground to wait for completion
|
||||||
)
|
)
|
||||||
|
|
||||||
theSession.DeleteUndoMark(markId_solve2, None)
|
theSession.DeleteUndoMark(markId_solve2, None)
|
||||||
@@ -718,14 +910,11 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
|||||||
|
|
||||||
print(f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped")
|
print(f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped")
|
||||||
|
|
||||||
# Save
|
# Save all
|
||||||
try:
|
try:
|
||||||
partSaveStatus = workSimPart.Save(
|
anyPartsModified, partSaveStatus = theSession.Parts.SaveAll()
|
||||||
NXOpen.BasePart.SaveComponents.TrueValue,
|
|
||||||
NXOpen.BasePart.CloseAfterSave.FalseValue
|
|
||||||
)
|
|
||||||
partSaveStatus.Dispose()
|
partSaveStatus.Dispose()
|
||||||
print(f"[JOURNAL] Saved!")
|
print(f"[JOURNAL] Saved all parts!")
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
1745
optimization_engine/study_wizard.py
Normal file
1745
optimization_engine/study_wizard.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
studies/bracket_pareto_3obj/1_setup/model/Bracket.prt
Normal file
BIN
studies/bracket_pareto_3obj/1_setup/model/Bracket.prt
Normal file
Binary file not shown.
BIN
studies/bracket_pareto_3obj/1_setup/model/Bracket_fem1.fem
Normal file
BIN
studies/bracket_pareto_3obj/1_setup/model/Bracket_fem1.fem
Normal file
Binary file not shown.
BIN
studies/bracket_pareto_3obj/1_setup/model/Bracket_fem1_i.prt
Normal file
BIN
studies/bracket_pareto_3obj/1_setup/model/Bracket_fem1_i.prt
Normal file
Binary file not shown.
BIN
studies/bracket_pareto_3obj/1_setup/model/Bracket_sim1.sim
Normal file
BIN
studies/bracket_pareto_3obj/1_setup/model/Bracket_sim1.sim
Normal file
Binary file not shown.
146
studies/bracket_pareto_3obj/1_setup/optimization_config.json
Normal file
146
studies/bracket_pareto_3obj/1_setup/optimization_config.json
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
{
|
||||||
|
"study_name": "bracket_pareto_3obj",
|
||||||
|
"description": "Three-objective Pareto optimization: minimize mass, minimize stress, maximize stiffness",
|
||||||
|
"engineering_context": "Generated by StudyWizard on 2025-12-06 14:43",
|
||||||
|
"template_info": {
|
||||||
|
"category": "structural",
|
||||||
|
"analysis_type": "static",
|
||||||
|
"typical_applications": [],
|
||||||
|
"neural_enabled": false
|
||||||
|
},
|
||||||
|
"optimization_settings": {
|
||||||
|
"protocol": "protocol_11_multi",
|
||||||
|
"n_trials": 100,
|
||||||
|
"sampler": "NSGAIISampler",
|
||||||
|
"pruner": null,
|
||||||
|
"timeout_per_trial": 400
|
||||||
|
},
|
||||||
|
"design_variables": [
|
||||||
|
{
|
||||||
|
"parameter": "support_angle",
|
||||||
|
"bounds": [
|
||||||
|
20,
|
||||||
|
70
|
||||||
|
],
|
||||||
|
"description": "Angle of support arm relative to base",
|
||||||
|
"units": "degrees"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameter": "tip_thickness",
|
||||||
|
"bounds": [
|
||||||
|
30,
|
||||||
|
60
|
||||||
|
],
|
||||||
|
"description": "Thickness at bracket tip where load is applied",
|
||||||
|
"units": "mm"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"objectives": [
|
||||||
|
{
|
||||||
|
"name": "mass",
|
||||||
|
"goal": "minimize",
|
||||||
|
"weight": 1.0,
|
||||||
|
"description": "Total bracket mass (kg)",
|
||||||
|
"extraction": {
|
||||||
|
"action": "extract_mass_from_bdf",
|
||||||
|
"domain": "result_extraction",
|
||||||
|
"params": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stress",
|
||||||
|
"goal": "minimize",
|
||||||
|
"weight": 1.0,
|
||||||
|
"description": "Maximum von Mises stress (MPa)",
|
||||||
|
"extraction": {
|
||||||
|
"action": "extract_solid_stress",
|
||||||
|
"domain": "result_extraction",
|
||||||
|
"params": {
|
||||||
|
"metric": "max_von_mises"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stiffness",
|
||||||
|
"goal": "maximize",
|
||||||
|
"weight": 1.0,
|
||||||
|
"description": "Structural stiffness = Force/Displacement (N/mm)",
|
||||||
|
"extraction": {
|
||||||
|
"action": "extract_displacement",
|
||||||
|
"domain": "result_extraction",
|
||||||
|
"params": {
|
||||||
|
"invert_for_stiffness": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"constraints": [
|
||||||
|
{
|
||||||
|
"name": "stress_limit",
|
||||||
|
"type": "less_than",
|
||||||
|
"threshold": 300,
|
||||||
|
"description": "Keep stress below 300 MPa for safety margin",
|
||||||
|
"extraction": {
|
||||||
|
"action": "extract_solid_stress",
|
||||||
|
"domain": "result_extraction",
|
||||||
|
"params": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"simulation": {
|
||||||
|
"model_file": "Bracket.prt",
|
||||||
|
"sim_file": "Bracket_sim1.sim",
|
||||||
|
"fem_file": "Bracket_fem1.fem",
|
||||||
|
"solver": "nastran",
|
||||||
|
"analysis_types": [
|
||||||
|
"static"
|
||||||
|
],
|
||||||
|
"solution_name": "Solution 1",
|
||||||
|
"dat_file": "bracket_sim1-solution_1.dat",
|
||||||
|
"op2_file": "bracket_sim1-solution_1.op2"
|
||||||
|
},
|
||||||
|
"result_extraction": {
|
||||||
|
"mass": {
|
||||||
|
"method": "extract_mass_from_bdf",
|
||||||
|
"extractor_module": "optimization_engine.extractors.bdf_mass_extractor",
|
||||||
|
"function": "extract_mass_from_bdf",
|
||||||
|
"output_unit": "kg"
|
||||||
|
},
|
||||||
|
"stress": {
|
||||||
|
"method": "extract_solid_stress",
|
||||||
|
"extractor_module": "optimization_engine.extractors.extract_von_mises_stress",
|
||||||
|
"function": "extract_solid_stress",
|
||||||
|
"output_unit": "MPa"
|
||||||
|
},
|
||||||
|
"stiffness": {
|
||||||
|
"method": "extract_displacement",
|
||||||
|
"extractor_module": "optimization_engine.extractors.extract_displacement",
|
||||||
|
"function": "extract_displacement",
|
||||||
|
"output_unit": "mm"
|
||||||
|
},
|
||||||
|
"stress_limit": {
|
||||||
|
"method": "extract_solid_stress",
|
||||||
|
"extractor_module": "optimization_engine.extractors.extract_von_mises_stress",
|
||||||
|
"function": "extract_solid_stress",
|
||||||
|
"output_unit": "MPa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reporting": {
|
||||||
|
"generate_plots": true,
|
||||||
|
"save_incremental": true,
|
||||||
|
"llm_summary": true,
|
||||||
|
"generate_pareto_front": true
|
||||||
|
},
|
||||||
|
"neural_acceleration": {
|
||||||
|
"enabled": true,
|
||||||
|
"min_training_points": 50,
|
||||||
|
"auto_train": true,
|
||||||
|
"epochs": 300,
|
||||||
|
"validation_split": 0.2,
|
||||||
|
"nn_trials": 1000,
|
||||||
|
"validate_top_n": 10,
|
||||||
|
"model_file": "surrogate_best.pt",
|
||||||
|
"separate_nn_database": true,
|
||||||
|
"description": "NN results stored in nn_study.db to avoid overloading dashboard"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
studies/bracket_pareto_3obj/1_setup/workflow_config.json
Normal file
5
studies/bracket_pareto_3obj/1_setup/workflow_config.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"workflow_id": "bracket_pareto_3obj_workflow",
|
||||||
|
"description": "Workflow for bracket_pareto_3obj",
|
||||||
|
"steps": []
|
||||||
|
}
|
||||||
228
studies/bracket_pareto_3obj/2_results/nn_optimization_state.json
Normal file
228
studies/bracket_pareto_3obj/2_results/nn_optimization_state.json
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
{
|
||||||
|
"phase": "nn_optimization",
|
||||||
|
"timestamp": "2025-12-06T19:05:54.740375",
|
||||||
|
"n_trials": 1000,
|
||||||
|
"n_pareto": 661,
|
||||||
|
"best_candidates": [
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 38.72700594236812,
|
||||||
|
"tip_thickness": 58.52142919229749
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.15462589263916016,
|
||||||
|
90.49411010742188,
|
||||||
|
-19956.513671875
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 56.59969709057025,
|
||||||
|
"tip_thickness": 47.959754525911094
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.1316341757774353,
|
||||||
|
80.95538330078125,
|
||||||
|
-15403.2138671875
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 27.800932022121827,
|
||||||
|
"tip_thickness": 34.67983561008608
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.1059565469622612,
|
||||||
|
75.57935333251953,
|
||||||
|
-8278.44921875
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 50.05575058716044,
|
||||||
|
"tip_thickness": 51.242177333881365
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.13515426218509674,
|
||||||
|
73.69579315185547,
|
||||||
|
-15871.068359375
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 29.09124836035503,
|
||||||
|
"tip_thickness": 35.50213529560301
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.10616718232631683,
|
||||||
|
75.49954986572266,
|
||||||
|
-8333.7919921875
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 41.59725093210579,
|
||||||
|
"tip_thickness": 38.736874205941255
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.10606641322374344,
|
||||||
|
77.42456817626953,
|
||||||
|
-8482.6328125
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 50.59264473611897,
|
||||||
|
"tip_thickness": 34.18481581956125
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.11001653969287872,
|
||||||
|
78.32686614990234,
|
||||||
|
-9909.66015625
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 34.60723242676091,
|
||||||
|
"tip_thickness": 40.99085529881075
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.11470890045166016,
|
||||||
|
71.76973724365234,
|
||||||
|
-10232.564453125
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 42.8034992108518,
|
||||||
|
"tip_thickness": 53.55527884179041
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.1554829478263855,
|
||||||
|
89.65568542480469,
|
||||||
|
-20128.802734375
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 49.620728443102124,
|
||||||
|
"tip_thickness": 31.393512381599933
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.10854113101959229,
|
||||||
|
78.32325744628906,
|
||||||
|
-9371.779296875
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 50.37724259507192,
|
||||||
|
"tip_thickness": 35.115723710618745
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.11040062457323074,
|
||||||
|
78.3082275390625,
|
||||||
|
-10054.8271484375
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 68.28160165372796,
|
||||||
|
"tip_thickness": 54.25192044349383
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.15124832093715668,
|
||||||
|
83.46127319335938,
|
||||||
|
-19232.740234375
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 35.23068845866854,
|
||||||
|
"tip_thickness": 32.93016342019152
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.10423046350479126,
|
||||||
|
77.35694122314453,
|
||||||
|
-7934.9453125
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 47.33551396716398,
|
||||||
|
"tip_thickness": 35.54563366576581
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.10879749059677124,
|
||||||
|
78.18163299560547,
|
||||||
|
-9440.0771484375
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 68.47923138822793,
|
||||||
|
"tip_thickness": 53.253984700833435
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.14725860953330994,
|
||||||
|
82.43916320800781,
|
||||||
|
-18467.29296875
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 66.97494707820945,
|
||||||
|
"tip_thickness": 56.844820512829465
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.15847891569137573,
|
||||||
|
86.1897201538086,
|
||||||
|
-20743.28515625
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 49.89499894055426,
|
||||||
|
"tip_thickness": 57.6562270506935
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.1606408655643463,
|
||||||
|
90.43415832519531,
|
||||||
|
-21159.50390625
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 24.424625102595975,
|
||||||
|
"tip_thickness": 35.87948587257436
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.10864812880754471,
|
||||||
|
73.66149139404297,
|
||||||
|
-8813.439453125
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 39.4338644844741,
|
||||||
|
"tip_thickness": 38.14047095321688
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.10515307635068893,
|
||||||
|
77.20490264892578,
|
||||||
|
-8183.75244140625
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 55.34286719238086,
|
||||||
|
"tip_thickness": 51.87021504122962
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.14633406698703766,
|
||||||
|
79.53317260742188,
|
||||||
|
-18268.1171875
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
7940
studies/bracket_pareto_3obj/2_results/nn_pareto_front.json
Normal file
7940
studies/bracket_pareto_3obj/2_results/nn_pareto_front.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
studies/bracket_pareto_3obj/2_results/nn_study.db
Normal file
BIN
studies/bracket_pareto_3obj/2_results/nn_study.db
Normal file
Binary file not shown.
1013
studies/bracket_pareto_3obj/2_results/pareto_front.json
Normal file
1013
studies/bracket_pareto_3obj/2_results/pareto_front.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
studies/bracket_pareto_3obj/2_results/study.db
Normal file
BIN
studies/bracket_pareto_3obj/2_results/study.db
Normal file
Binary file not shown.
BIN
studies/bracket_pareto_3obj/2_results/surrogate_best.pt
Normal file
BIN
studies/bracket_pareto_3obj/2_results/surrogate_best.pt
Normal file
Binary file not shown.
2095
studies/bracket_pareto_3obj/2_results/training_data.json
Normal file
2095
studies/bracket_pareto_3obj/2_results/training_data.json
Normal file
File diff suppressed because it is too large
Load Diff
328
studies/bracket_pareto_3obj/2_results/turbo_report.json
Normal file
328
studies/bracket_pareto_3obj/2_results/turbo_report.json
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
{
|
||||||
|
"mode": "turbo",
|
||||||
|
"total_nn_trials": 5000,
|
||||||
|
"fea_validations": 50,
|
||||||
|
"time_minutes": 12.065277910232544,
|
||||||
|
"best_solutions": [
|
||||||
|
{
|
||||||
|
"iteration": 31,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 31.847281190596824,
|
||||||
|
"tip_thickness": 32.91164052283733
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.10370742238857288,
|
||||||
|
75.331484375,
|
||||||
|
-7673.294824045775
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
1.0860589212456762,
|
||||||
|
1.8689438405308587
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 32,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 35.78134982929724,
|
||||||
|
"tip_thickness": 35.42681622195606
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.10953498495777715,
|
||||||
|
74.246125,
|
||||||
|
-9104.355438099408
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
5.9983784586009286,
|
||||||
|
3.442366247886034
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 33,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 30.994512918956225,
|
||||||
|
"tip_thickness": 31.052314916198533
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.0998217013424325,
|
||||||
|
77.4071796875,
|
||||||
|
-6775.567320757415
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
2.62213154769254,
|
||||||
|
0.6237176551876354
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 34,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 33.099819835866754,
|
||||||
|
"tip_thickness": 32.89301733006174
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.10396239429164271,
|
||||||
|
75.584921875,
|
||||||
|
-7760.270535172856
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
1.3055871373511414,
|
||||||
|
1.7371954997844847
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 35,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 30.898541287011337,
|
||||||
|
"tip_thickness": 34.418250550014
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.1065015994297987,
|
||||||
|
74.408234375,
|
||||||
|
-8241.342422091839
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
2.9174895410063533,
|
||||||
|
2.2559274228984143
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 36,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 33.473891105805734,
|
||||||
|
"tip_thickness": 34.16062542894516
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.10656349355439027,
|
||||||
|
75.102046875,
|
||||||
|
-8326.35651590611
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
3.6174682481860545,
|
||||||
|
2.1680046671133515
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 37,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 31.876112833251945,
|
||||||
|
"tip_thickness": 32.64558622955443
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.10316854746371616,
|
||||||
|
76.0821640625,
|
||||||
|
-7551.884666556311
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
0.616586592199277,
|
||||||
|
0.9385311503281267
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 38,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 30.714982000638024,
|
||||||
|
"tip_thickness": 30.67768874508055
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.09900839247305124,
|
||||||
|
77.738234375,
|
||||||
|
-6613.818689996269
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
3.445733195248999,
|
||||||
|
1.0253383054399168
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 39,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 28.913554019167456,
|
||||||
|
"tip_thickness": 30.483198120379658
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.09815608468915514,
|
||||||
|
77.3044140625,
|
||||||
|
-6401.798601024496
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
4.31900669557528,
|
||||||
|
0.6715572168522086
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 40,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 30.64103130907421,
|
||||||
|
"tip_thickness": 32.225435935347505
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.10203815917423766,
|
||||||
|
76.404703125,
|
||||||
|
-7263.383668463729
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
0.5053920341375967,
|
||||||
|
0.3872153898156662
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 41,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 25.379887341054648,
|
||||||
|
"tip_thickness": 31.7995059368559
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.09989812757495894,
|
||||||
|
76.9576796875,
|
||||||
|
-6664.024314617181
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
4.447284090430112,
|
||||||
|
1.5796573759898327
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 42,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 31.731587709716017,
|
||||||
|
"tip_thickness": 30.897825980216872
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.09972626857174226,
|
||||||
|
77.77390625,
|
||||||
|
-6787.919099905275
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
3.6536017763654174,
|
||||||
|
1.4414725087041111
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 43,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 33.10878057556627,
|
||||||
|
"tip_thickness": 33.355298773540355
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.1048663080654111,
|
||||||
|
75.480953125,
|
||||||
|
-7947.3954282813875
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
1.1127142382050441,
|
||||||
|
1.24740755881399
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 44,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 33.486603646649684,
|
||||||
|
"tip_thickness": 30.362623804600066
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.09923041195413426,
|
||||||
|
79.016015625,
|
||||||
|
-6713.039943213783
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
4.287407722991723,
|
||||||
|
2.630846755256295
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 45,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 28.114078180607912,
|
||||||
|
"tip_thickness": 31.737991396793802
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.10039508543743812,
|
||||||
|
77.6226171875,
|
||||||
|
-6820.132648794927
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
3.5140537947946973,
|
||||||
|
1.8965874116002928
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 46,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 32.00933223521479,
|
||||||
|
"tip_thickness": 30.3146054439274
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.09865586146399362,
|
||||||
|
78.773390625,
|
||||||
|
-6537.562541889428
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
4.747051326710379,
|
||||||
|
2.548631636595247
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 47,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 33.13530006102697,
|
||||||
|
"tip_thickness": 33.39675764700238
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.10495349474799269,
|
||||||
|
75.4744296875,
|
||||||
|
-7967.975581083746
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
1.1881318255229905,
|
||||||
|
1.2499923821726795
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 48,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 31.37280375169122,
|
||||||
|
"tip_thickness": 32.20022793873885
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.10217431187937046,
|
||||||
|
76.5387421875,
|
||||||
|
-7300.86967873889
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
1.4111097241955246,
|
||||||
|
0.18087978882019146
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 49,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 31.633966114017845,
|
||||||
|
"tip_thickness": 30.14620749968385
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.0982228321492226,
|
||||||
|
78.6505,
|
||||||
|
-6436.600331762441
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
5.183933182520313,
|
||||||
|
2.4268434241418446
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iteration": 50,
|
||||||
|
"params": {
|
||||||
|
"support_angle": 30.835096541574387,
|
||||||
|
"tip_thickness": 31.83135554844258
|
||||||
|
},
|
||||||
|
"fea": [
|
||||||
|
0.10131094537705086,
|
||||||
|
76.825890625,
|
||||||
|
-7117.327055357855
|
||||||
|
],
|
||||||
|
"nn_error": [
|
||||||
|
2.2561942677161455,
|
||||||
|
0.5555181135021817
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
221
studies/bracket_pareto_3obj/2_results/validation_report.json
Normal file
221
studies/bracket_pareto_3obj/2_results/validation_report.json
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-06T19:08:19.427388",
|
||||||
|
"n_validated": 10,
|
||||||
|
"average_errors_percent": {
|
||||||
|
"mass": 3.718643367122823,
|
||||||
|
"stress": 2.020364475341075,
|
||||||
|
"stiffness": 7.782164972196007
|
||||||
|
},
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 38.72700594236812,
|
||||||
|
"tip_thickness": 58.52142919229749
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.15462589263916016,
|
||||||
|
90.49411010742188,
|
||||||
|
-19956.513671875
|
||||||
|
],
|
||||||
|
"fea_objectives": [
|
||||||
|
0.1594800904665372,
|
||||||
|
89.4502578125,
|
||||||
|
-20960.59592691965
|
||||||
|
],
|
||||||
|
"errors_percent": [
|
||||||
|
3.0437641546206433,
|
||||||
|
1.1669639869679682,
|
||||||
|
4.790332577114896
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 56.59969709057025,
|
||||||
|
"tip_thickness": 47.959754525911094
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.1316341757774353,
|
||||||
|
80.95538330078125,
|
||||||
|
-15403.2138671875
|
||||||
|
],
|
||||||
|
"fea_objectives": [
|
||||||
|
0.1370984916826118,
|
||||||
|
80.2043046875,
|
||||||
|
-16381.0655256764
|
||||||
|
],
|
||||||
|
"errors_percent": [
|
||||||
|
3.9856863763509414,
|
||||||
|
0.9364567353431696,
|
||||||
|
5.969402032829749
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 27.800932022121827,
|
||||||
|
"tip_thickness": 34.67983561008608
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.1059565469622612,
|
||||||
|
75.57935333251953,
|
||||||
|
-8278.44921875
|
||||||
|
],
|
||||||
|
"fea_objectives": [
|
||||||
|
0.10630918746092984,
|
||||||
|
75.5471015625,
|
||||||
|
-8142.120566330409
|
||||||
|
],
|
||||||
|
"errors_percent": [
|
||||||
|
0.3317121568615468,
|
||||||
|
0.0426909429382223,
|
||||||
|
1.6743629784032203
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 50.05575058716044,
|
||||||
|
"tip_thickness": 51.242177333881365
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.13515426218509674,
|
||||||
|
73.69579315185547,
|
||||||
|
-15871.068359375
|
||||||
|
],
|
||||||
|
"fea_objectives": [
|
||||||
|
0.14318368576930707,
|
||||||
|
73.3545859375,
|
||||||
|
-17662.840771637857
|
||||||
|
],
|
||||||
|
"errors_percent": [
|
||||||
|
5.607778247269787,
|
||||||
|
0.46514776137674774,
|
||||||
|
10.14430484557161
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 29.09124836035503,
|
||||||
|
"tip_thickness": 35.50213529560301
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.10616718232631683,
|
||||||
|
75.49954986572266,
|
||||||
|
-8333.7919921875
|
||||||
|
],
|
||||||
|
"fea_objectives": [
|
||||||
|
0.10827058249925942,
|
||||||
|
72.4169921875,
|
||||||
|
-8632.595914157022
|
||||||
|
],
|
||||||
|
"errors_percent": [
|
||||||
|
1.9427254609597853,
|
||||||
|
4.256677314409008,
|
||||||
|
3.461344941207066
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 41.59725093210579,
|
||||||
|
"tip_thickness": 38.736874205941255
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.10606641322374344,
|
||||||
|
77.42456817626953,
|
||||||
|
-8482.6328125
|
||||||
|
],
|
||||||
|
"fea_objectives": [
|
||||||
|
0.11718762744364532,
|
||||||
|
75.1669609375,
|
||||||
|
-11092.555729424334
|
||||||
|
],
|
||||||
|
"errors_percent": [
|
||||||
|
9.490092480326041,
|
||||||
|
3.0034568520692204,
|
||||||
|
23.52859864387429
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 50.59264473611897,
|
||||||
|
"tip_thickness": 34.18481581956125
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.11001653969287872,
|
||||||
|
78.32686614990234,
|
||||||
|
-9909.66015625
|
||||||
|
],
|
||||||
|
"fea_objectives": [
|
||||||
|
0.11190565081078178,
|
||||||
|
76.7876328125,
|
||||||
|
-10422.469553635548
|
||||||
|
],
|
||||||
|
"errors_percent": [
|
||||||
|
1.6881284405354184,
|
||||||
|
2.0045328668496007,
|
||||||
|
4.920229267608377
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 34.60723242676091,
|
||||||
|
"tip_thickness": 40.99085529881075
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.11470890045166016,
|
||||||
|
71.76973724365234,
|
||||||
|
-10232.564453125
|
||||||
|
],
|
||||||
|
"fea_objectives": [
|
||||||
|
0.12047991649273775,
|
||||||
|
70.5054453125,
|
||||||
|
-11692.113952912616
|
||||||
|
],
|
||||||
|
"errors_percent": [
|
||||||
|
4.790023274481149,
|
||||||
|
1.7931833854089492,
|
||||||
|
12.483195987189537
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 42.8034992108518,
|
||||||
|
"tip_thickness": 53.55527884179041
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.1554829478263855,
|
||||||
|
89.65568542480469,
|
||||||
|
-20128.802734375
|
||||||
|
],
|
||||||
|
"fea_objectives": [
|
||||||
|
0.14802894076279258,
|
||||||
|
92.6986484375,
|
||||||
|
-18351.580922756133
|
||||||
|
],
|
||||||
|
"errors_percent": [
|
||||||
|
5.03550658755136,
|
||||||
|
3.282640107473584,
|
||||||
|
9.684298149022656
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": {
|
||||||
|
"support_angle": 49.620728443102124,
|
||||||
|
"tip_thickness": 31.393512381599933
|
||||||
|
},
|
||||||
|
"nn_objectives": [
|
||||||
|
0.10854113101959229,
|
||||||
|
78.32325744628906,
|
||||||
|
-9371.779296875
|
||||||
|
],
|
||||||
|
"fea_objectives": [
|
||||||
|
0.107178869906846,
|
||||||
|
75.856484375,
|
||||||
|
-9263.802242979662
|
||||||
|
],
|
||||||
|
"errors_percent": [
|
||||||
|
1.2710164922715559,
|
||||||
|
3.2518948005742834,
|
||||||
|
1.1655802991386794
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
60
studies/bracket_pareto_3obj/MODEL_INTROSPECTION.md
Normal file
60
studies/bracket_pareto_3obj/MODEL_INTROSPECTION.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Model Introspection Report
|
||||||
|
|
||||||
|
**Study**: bracket_pareto_3obj
|
||||||
|
**Generated**: 2025-12-06 14:43
|
||||||
|
**Introspection Version**: 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Files Discovered
|
||||||
|
|
||||||
|
| Type | File | Status |
|
||||||
|
|------|------|--------|
|
||||||
|
| Part (.prt) | Bracket.prt | ✓ Found |
|
||||||
|
| Simulation (.sim) | Bracket_sim1.sim | ✓ Found |
|
||||||
|
| FEM (.fem) | Bracket_fem1.fem | ✓ Found |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Expressions (Potential Design Variables)
|
||||||
|
|
||||||
|
*Run introspection to discover expressions.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Solutions
|
||||||
|
|
||||||
|
*Run introspection to discover solutions.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Available Results
|
||||||
|
|
||||||
|
| Result Type | Available | Subcases |
|
||||||
|
|-------------|-----------|----------|
|
||||||
|
| Displacement | ? | - |
|
||||||
|
| Stress | ? | - |
|
||||||
|
| SPC Forces | ? | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Optimization Configuration
|
||||||
|
|
||||||
|
### Selected Design Variables
|
||||||
|
|
||||||
|
- `support_angle`: [20, 70] degrees
|
||||||
|
- `tip_thickness`: [30, 60] mm
|
||||||
|
|
||||||
|
### Selected Objectives
|
||||||
|
|
||||||
|
- Minimize `mass` using `extract_mass_from_bdf`
|
||||||
|
- Minimize `stress` using `extract_solid_stress`
|
||||||
|
- Maximize `stiffness` using `extract_displacement`
|
||||||
|
|
||||||
|
### Selected Constraints
|
||||||
|
|
||||||
|
- `stress_limit` less_than 300 MPa
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Ready to create optimization study? Run `python run_optimization.py --discover` to proceed.*
|
||||||
130
studies/bracket_pareto_3obj/README.md
Normal file
130
studies/bracket_pareto_3obj/README.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# bracket_pareto_3obj
|
||||||
|
|
||||||
|
Three-objective Pareto optimization: minimize mass, minimize stress, maximize stiffness
|
||||||
|
|
||||||
|
**Generated**: 2025-12-06 14:43
|
||||||
|
**Protocol**: Multi-Objective NSGA-II
|
||||||
|
**Trials**: 100
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Engineering Problem
|
||||||
|
|
||||||
|
Three-objective Pareto optimization: minimize mass, minimize stress, maximize stiffness
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Mathematical Formulation
|
||||||
|
|
||||||
|
### Design Variables
|
||||||
|
|
||||||
|
| Parameter | Bounds | Units | Description |
|
||||||
|
|-----------|--------|-------|-------------|
|
||||||
|
| `support_angle` | [20, 70] | degrees | Angle of support arm relative to base |
|
||||||
|
| `tip_thickness` | [30, 60] | mm | Thickness at bracket tip where load is applied |
|
||||||
|
|
||||||
|
|
||||||
|
### Objectives
|
||||||
|
|
||||||
|
| Objective | Goal | Extractor | Weight |
|
||||||
|
|-----------|------|-----------|--------|
|
||||||
|
| mass | minimize | `extract_mass_from_bdf` | 1.0 |
|
||||||
|
| stress | minimize | `extract_solid_stress` | 1.0 |
|
||||||
|
| stiffness | maximize | `extract_displacement` | 1.0 |
|
||||||
|
|
||||||
|
|
||||||
|
### Constraints
|
||||||
|
|
||||||
|
| Constraint | Type | Threshold | Units |
|
||||||
|
|------------|------|-----------|-------|
|
||||||
|
| stress_limit | less_than | 300 | MPa |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Optimization Algorithm
|
||||||
|
|
||||||
|
- **Protocol**: protocol_11_multi
|
||||||
|
- **Sampler**: NSGAIISampler
|
||||||
|
- **Trials**: 100
|
||||||
|
- **Neural Acceleration**: Disabled
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Simulation Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
Design Variables → NX Expression Update → Nastran Solve → Result Extraction → Objective Evaluation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Result Extraction Methods
|
||||||
|
|
||||||
|
| Result | Extractor | Source |
|
||||||
|
|--------|-----------|--------|
|
||||||
|
| mass | `extract_mass_from_bdf` | OP2/DAT |
|
||||||
|
| stress | `extract_solid_stress` | OP2/DAT |
|
||||||
|
| stiffness | `extract_displacement` | OP2/DAT |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Study File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
bracket_pareto_3obj/
|
||||||
|
├── 1_setup/
|
||||||
|
│ ├── model/
|
||||||
|
│ │ ├── Bracket.prt
|
||||||
|
│ │ ├── Bracket_sim1.sim
|
||||||
|
│ │ └── Bracket_fem1.fem
|
||||||
|
│ ├── optimization_config.json
|
||||||
|
│ └── workflow_config.json
|
||||||
|
├── 2_results/
|
||||||
|
│ ├── study.db
|
||||||
|
│ └── optimization.log
|
||||||
|
├── run_optimization.py
|
||||||
|
├── reset_study.py
|
||||||
|
├── README.md
|
||||||
|
├── STUDY_REPORT.md
|
||||||
|
└── MODEL_INTROSPECTION.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Discover model outputs
|
||||||
|
python run_optimization.py --discover
|
||||||
|
|
||||||
|
# 2. Validate setup with single trial
|
||||||
|
python run_optimization.py --validate
|
||||||
|
|
||||||
|
# 3. Run integration test (3 trials)
|
||||||
|
python run_optimization.py --test
|
||||||
|
|
||||||
|
# 4. Run full optimization
|
||||||
|
python run_optimization.py --run --trials 100
|
||||||
|
|
||||||
|
# 5. Resume if interrupted
|
||||||
|
python run_optimization.py --run --trials 50 --resume
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Results Location
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `2_results/study.db` | Optuna SQLite database |
|
||||||
|
| `2_results/optimization.log` | Structured log file |
|
||||||
|
| `2_results/pareto_front.json` | Pareto-optimal solutions |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. References
|
||||||
|
|
||||||
|
- [Atomizer Documentation](../../docs/)
|
||||||
|
- [Protocol protocol_11_multi](../../docs/protocols/system/)
|
||||||
|
- [Extractor Library](../../docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md)
|
||||||
60
studies/bracket_pareto_3obj/STUDY_REPORT.md
Normal file
60
studies/bracket_pareto_3obj/STUDY_REPORT.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Study Report: bracket_pareto_3obj
|
||||||
|
|
||||||
|
**Status**: Not Started
|
||||||
|
**Created**: 2025-12-06 14:43
|
||||||
|
**Last Updated**: 2025-12-06 14:43
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Optimization Progress
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Total Trials | 0 |
|
||||||
|
| Successful Trials | 0 |
|
||||||
|
| Best Objective | - |
|
||||||
|
| Duration | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Best Solutions
|
||||||
|
|
||||||
|
*No optimization runs completed yet.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Pareto Front (if multi-objective)
|
||||||
|
|
||||||
|
*No Pareto front generated yet.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Design Variable Sensitivity
|
||||||
|
|
||||||
|
*Analysis pending optimization runs.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Constraint Satisfaction
|
||||||
|
|
||||||
|
*Analysis pending optimization runs.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Recommendations
|
||||||
|
|
||||||
|
*Recommendations will be added after optimization runs.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Next Steps
|
||||||
|
|
||||||
|
1. [ ] Run `python run_optimization.py --discover`
|
||||||
|
2. [ ] Run `python run_optimization.py --validate`
|
||||||
|
3. [ ] Run `python run_optimization.py --test`
|
||||||
|
4. [ ] Run `python run_optimization.py --run --trials 100`
|
||||||
|
5. [ ] Analyze results and update this report
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Generated by StudyWizard*
|
||||||
48
studies/bracket_pareto_3obj/reset_study.py
Normal file
48
studies/bracket_pareto_3obj/reset_study.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""
|
||||||
|
Reset study - Delete results database and logs.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python reset_study.py
|
||||||
|
python reset_study.py --confirm # Skip confirmation
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--confirm', action='store_true', help='Skip confirmation')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
study_dir = Path(__file__).parent
|
||||||
|
results_dir = study_dir / "2_results"
|
||||||
|
|
||||||
|
if not args.confirm:
|
||||||
|
print(f"This will delete all results in: {results_dir}")
|
||||||
|
response = input("Are you sure? (y/N): ")
|
||||||
|
if response.lower() != 'y':
|
||||||
|
print("Cancelled.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Delete database files
|
||||||
|
for f in results_dir.glob("*.db"):
|
||||||
|
f.unlink()
|
||||||
|
print(f"Deleted: {f.name}")
|
||||||
|
|
||||||
|
# Delete log files
|
||||||
|
for f in results_dir.glob("*.log"):
|
||||||
|
f.unlink()
|
||||||
|
print(f"Deleted: {f.name}")
|
||||||
|
|
||||||
|
# Delete JSON results
|
||||||
|
for f in results_dir.glob("*.json"):
|
||||||
|
f.unlink()
|
||||||
|
print(f"Deleted: {f.name}")
|
||||||
|
|
||||||
|
print("Study reset complete.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1095
studies/bracket_pareto_3obj/run_nn_optimization.py
Normal file
1095
studies/bracket_pareto_3obj/run_nn_optimization.py
Normal file
File diff suppressed because it is too large
Load Diff
245
studies/bracket_pareto_3obj/run_optimization.py
Normal file
245
studies/bracket_pareto_3obj/run_optimization.py
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
"""
|
||||||
|
bracket_pareto_3obj - Optimization Script
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Three-objective Pareto optimization: minimize mass, minimize stress, maximize stiffness
|
||||||
|
|
||||||
|
Protocol: Multi-Objective NSGA-II
|
||||||
|
|
||||||
|
Staged Workflow:
|
||||||
|
----------------
|
||||||
|
1. DISCOVER: python run_optimization.py --discover
|
||||||
|
2. VALIDATE: python run_optimization.py --validate
|
||||||
|
3. TEST: python run_optimization.py --test
|
||||||
|
4. RUN: python run_optimization.py --run --trials 100
|
||||||
|
|
||||||
|
Generated by StudyWizard on 2025-12-06 14:43
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Tuple, List
|
||||||
|
|
||||||
|
# Add parent directory to path
|
||||||
|
project_root = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
import optuna
|
||||||
|
from optuna.samplers import NSGAIISampler
|
||||||
|
|
||||||
|
# Core imports
|
||||||
|
from optimization_engine.nx_solver import NXSolver
|
||||||
|
from optimization_engine.logger import get_logger
|
||||||
|
|
||||||
|
# Extractor imports
|
||||||
|
from optimization_engine.extractors.bdf_mass_extractor import extract_mass_from_bdf
|
||||||
|
from optimization_engine.extractors.extract_displacement import extract_displacement
|
||||||
|
from optimization_engine.extractors.extract_von_mises_stress import extract_solid_stress
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config_file: Path) -> dict:
|
||||||
|
"""Load configuration from JSON file."""
|
||||||
|
with open(config_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_nastran_files(model_dir: Path, logger) -> List[Path]:
|
||||||
|
"""Remove old Nastran solver output files."""
|
||||||
|
patterns = ['*.op2', '*.f06', '*.log', '*.f04', '*.pch', '*.DBALL', '*.MASTER', '_temp*.txt']
|
||||||
|
deleted = []
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
for f in model_dir.glob(pattern):
|
||||||
|
try:
|
||||||
|
f.unlink()
|
||||||
|
deleted.append(f)
|
||||||
|
logger.info(f" Deleted: {f.name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f" Failed to delete {f.name}: {e}")
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
|
def objective(trial: optuna.Trial, config: dict, nx_solver: NXSolver,
|
||||||
|
model_dir: Path, logger) -> Tuple[float, float, float]:
|
||||||
|
"""
|
||||||
|
Objective function for optimization.
|
||||||
|
|
||||||
|
Returns tuple of objectives for multi-objective optimization.
|
||||||
|
"""
|
||||||
|
# Sample design variables
|
||||||
|
design_vars = {}
|
||||||
|
for var in config['design_variables']:
|
||||||
|
param_name = var['parameter']
|
||||||
|
bounds = var['bounds']
|
||||||
|
design_vars[param_name] = trial.suggest_float(param_name, bounds[0], bounds[1])
|
||||||
|
|
||||||
|
logger.trial_start(trial.number, design_vars)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get file paths
|
||||||
|
sim_file = model_dir / config['simulation']['sim_file']
|
||||||
|
|
||||||
|
# Run FEA simulation
|
||||||
|
result = nx_solver.run_simulation(
|
||||||
|
sim_file=sim_file,
|
||||||
|
working_dir=model_dir,
|
||||||
|
expression_updates=design_vars,
|
||||||
|
solution_name=config['simulation'].get('solution_name'),
|
||||||
|
cleanup=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result['success']:
|
||||||
|
logger.trial_failed(trial.number, f"Simulation failed: {result.get('error', 'Unknown')}")
|
||||||
|
return (float('inf'), float('inf'), float('inf'))
|
||||||
|
|
||||||
|
op2_file = result['op2_file']
|
||||||
|
dat_file = model_dir / config['simulation']['dat_file']
|
||||||
|
|
||||||
|
# Extract results
|
||||||
|
obj_mass = extract_mass_from_bdf(str(dat_file))
|
||||||
|
logger.info(f' mass: {obj_mass}')
|
||||||
|
|
||||||
|
stress_result = extract_solid_stress(op2_file, subcase=1, element_type='chexa')
|
||||||
|
obj_stress = stress_result.get('max_von_mises', float('inf')) / 1000.0 # kPa -> MPa
|
||||||
|
logger.info(f' stress: {obj_stress:.2f} MPa')
|
||||||
|
|
||||||
|
disp_result = extract_displacement(op2_file, subcase=1)
|
||||||
|
max_displacement = disp_result['max_displacement']
|
||||||
|
# For stiffness maximization, use inverse of displacement
|
||||||
|
applied_force = 1000.0 # N - adjust based on your model
|
||||||
|
obj_stiffness = -applied_force / max(abs(max_displacement), 1e-6)
|
||||||
|
logger.info(f' stiffness: {obj_stiffness}')
|
||||||
|
|
||||||
|
|
||||||
|
# Check constraints
|
||||||
|
feasible = True
|
||||||
|
constraint_results = {}
|
||||||
|
# Check stress_limit (stress from OP2 is in kPa for mm/kg units, convert to MPa)
|
||||||
|
const_stress_limit = extract_solid_stress(op2_file, element_type='chexa')
|
||||||
|
stress_mpa = const_stress_limit.get('max_von_mises', float('inf')) / 1000.0 # kPa -> MPa
|
||||||
|
constraint_results['stress_limit'] = stress_mpa
|
||||||
|
if stress_mpa > 300:
|
||||||
|
feasible = False
|
||||||
|
logger.warning(f' Constraint violation: stress_limit = {stress_mpa:.1f} MPa vs 300 MPa')
|
||||||
|
|
||||||
|
|
||||||
|
# Set user attributes
|
||||||
|
trial.set_user_attr('mass', obj_mass)
|
||||||
|
trial.set_user_attr('stress', obj_stress)
|
||||||
|
trial.set_user_attr('stiffness', obj_stiffness)
|
||||||
|
trial.set_user_attr('feasible', feasible)
|
||||||
|
|
||||||
|
objectives = {'mass': obj_mass, 'stress': obj_stress, 'stiffness': obj_stiffness}
|
||||||
|
logger.trial_complete(trial.number, objectives, constraint_results, feasible)
|
||||||
|
|
||||||
|
return (obj_mass, obj_stress, obj_stiffness)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.trial_failed(trial.number, str(e))
|
||||||
|
return (float('inf'), float('inf'), float('inf'))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main optimization workflow."""
|
||||||
|
parser = argparse.ArgumentParser(description='bracket_pareto_3obj')
|
||||||
|
|
||||||
|
stage_group = parser.add_mutually_exclusive_group()
|
||||||
|
stage_group.add_argument('--discover', action='store_true', help='Discover model outputs')
|
||||||
|
stage_group.add_argument('--validate', action='store_true', help='Run single validation trial')
|
||||||
|
stage_group.add_argument('--test', action='store_true', help='Run 3-trial test')
|
||||||
|
stage_group.add_argument('--run', action='store_true', help='Run optimization')
|
||||||
|
|
||||||
|
parser.add_argument('--trials', type=int, default=100, help='Number of trials')
|
||||||
|
parser.add_argument('--resume', action='store_true', help='Resume existing study')
|
||||||
|
parser.add_argument('--clean', action='store_true', help='Clean old files first')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not any([args.discover, args.validate, args.test, args.run]):
|
||||||
|
print("No stage specified. Use --discover, --validate, --test, or --run")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Setup paths
|
||||||
|
study_dir = Path(__file__).parent
|
||||||
|
config_path = study_dir / "1_setup" / "optimization_config.json"
|
||||||
|
model_dir = study_dir / "1_setup" / "model"
|
||||||
|
results_dir = study_dir / "2_results"
|
||||||
|
results_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
study_name = "bracket_pareto_3obj"
|
||||||
|
|
||||||
|
# Initialize
|
||||||
|
logger = get_logger(study_name, study_dir=results_dir)
|
||||||
|
config = load_config(config_path)
|
||||||
|
nx_solver = NXSolver(nastran_version="2506")
|
||||||
|
|
||||||
|
if args.clean:
|
||||||
|
clean_nastran_files(model_dir, logger)
|
||||||
|
|
||||||
|
# Run appropriate stage
|
||||||
|
if args.discover or args.validate or args.test:
|
||||||
|
# Run limited trials for these stages
|
||||||
|
n = 1 if args.discover or args.validate else 3
|
||||||
|
storage = f"sqlite:///{results_dir / 'study_test.db'}"
|
||||||
|
|
||||||
|
study = optuna.create_study(
|
||||||
|
study_name=f"{study_name}_test",
|
||||||
|
storage=storage,
|
||||||
|
sampler=NSGAIISampler(population_size=5, seed=42),
|
||||||
|
directions=['minimize'] * 3,
|
||||||
|
load_if_exists=False
|
||||||
|
)
|
||||||
|
|
||||||
|
study.optimize(
|
||||||
|
lambda trial: objective(trial, config, nx_solver, model_dir, logger),
|
||||||
|
n_trials=n,
|
||||||
|
show_progress_bar=True
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Completed {len(study.trials)} trial(s)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Full optimization run
|
||||||
|
storage = f"sqlite:///{results_dir / 'study.db'}"
|
||||||
|
|
||||||
|
if args.resume:
|
||||||
|
study = optuna.load_study(
|
||||||
|
study_name=study_name,
|
||||||
|
storage=storage,
|
||||||
|
sampler=NSGAIISampler(population_size=20, seed=42)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
study = optuna.create_study(
|
||||||
|
study_name=study_name,
|
||||||
|
storage=storage,
|
||||||
|
sampler=NSGAIISampler(population_size=20, seed=42),
|
||||||
|
directions=['minimize'] * 3,
|
||||||
|
load_if_exists=True
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.study_start(study_name, args.trials, "NSGAIISampler")
|
||||||
|
|
||||||
|
study.optimize(
|
||||||
|
lambda trial: objective(trial, config, nx_solver, model_dir, logger),
|
||||||
|
n_trials=args.trials,
|
||||||
|
show_progress_bar=True
|
||||||
|
)
|
||||||
|
|
||||||
|
n_complete = len([t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE])
|
||||||
|
logger.study_complete(study_name, len(study.trials), n_complete)
|
||||||
|
|
||||||
|
# Report results
|
||||||
|
pareto_trials = study.best_trials
|
||||||
|
logger.info(f"\nOptimization Complete!")
|
||||||
|
logger.info(f"Total trials: {len(study.trials)}")
|
||||||
|
logger.info(f"Successful: {n_complete}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
exit(main())
|
||||||
Reference in New Issue
Block a user