diff --git a/.claude/skills/00_BOOTSTRAP.md b/.claude/skills/00_BOOTSTRAP.md new file mode 100644 index 00000000..199f7154 --- /dev/null +++ b/.claude/skills/00_BOOTSTRAP.md @@ -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` diff --git a/.claude/skills/01_CHEATSHEET.md b/.claude/skills/01_CHEATSHEET.md new file mode 100644 index 00000000..779cbf95 --- /dev/null +++ b/.claude/skills/01_CHEATSHEET.md @@ -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 | diff --git a/.claude/skills/02_CONTEXT_LOADER.md b/.claude/skills/02_CONTEXT_LOADER.md new file mode 100644 index 00000000..6fc62a01 --- /dev/null +++ b/.claude/skills/02_CONTEXT_LOADER.md @@ -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 diff --git a/.claude/skills/DEV_DOCUMENTATION.md b/.claude/skills/DEV_DOCUMENTATION.md new file mode 100644 index 00000000..19fa7419 --- /dev/null +++ b/.claude/skills/DEV_DOCUMENTATION.md @@ -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 + + + +## 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 diff --git a/.claude/skills/PROTOCOL_EXECUTION.md b/.claude/skills/PROTOCOL_EXECUTION.md new file mode 100644 index 00000000..10565700 --- /dev/null +++ b/.claude/skills/PROTOCOL_EXECUTION.md @@ -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) diff --git a/.claude/skills/analyze-model.md b/.claude/skills/analyze-model.md index 91887182..b338dea3 100644 --- a/.claude/skills/analyze-model.md +++ b/.claude/skills/analyze-model.md @@ -1,7 +1,7 @@ # Analyze Model Skill -**Last Updated**: November 25, 2025 -**Version**: 1.0 - Model Analysis and Feature Extraction +**Last Updated**: December 6, 2025 +**Version**: 2.0 - Added Comprehensive Model Introspection 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) 2. Understand the simulation setup (analysis types, boundary conditions) 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 @@ -20,28 +21,107 @@ Extract and present information about an NX model to help the user: - "show me the expressions" - "look at my NX model" - "what parameters are available" +- "introspect my model" +- "what results are available" ## Prerequisites -- User must provide path to NX model files (.prt, .sim, .fem) -- NX must be available on the system (configured in config.py) -- Model files must be valid NX format +- User must provide path to NX model files (.prt, .sim, .fem) or study directory +- NX must be available on the system for part/sim introspection +- OP2 introspection works without NX (pure Python) ## Information Gathering Ask these questions if not already provided: 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/` 2. **Analysis Interest**: - "What type of optimization are you considering?" (optional) - 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 -### 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: @@ -95,26 +175,21 @@ def validate_model_files(model_path: Path) -> dict: 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 -# This requires running a journal inside NX -# Use the expression extractor from optimization_engine +from optimization_engine.hooks.nx_cad.model_introspection import introspect_part -from optimization_engine.extractors.expression_extractor import extract_all_expressions - -expressions = extract_all_expressions(prt_file) -# Returns: [{'name': 'thickness', 'value': 2.0, 'unit': 'mm', 'formula': None}, ...] +result = introspect_part("path/to/model.prt") +if result["success"]: + expressions = result["data"].get("expressions", []) + for expr in expressions: + print(f" {expr['name']}: {expr['value']} {expr.get('unit', '')}") ``` -**Manual Extraction Method** (if NX API not available): -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 +### Step 4: Classify Expressions Categorize expressions by likely purpose: @@ -154,53 +229,121 @@ Based on analysis, recommend: ## 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} -Location: {model_path} +### Saving the Report -FILES FOUND ------------ -✓ Part file: {prt_file} -✓ Simulation: {sim_file} -✓ FEM mesh: {fem_file} +**MANDATORY**: Save the introspection report to the study directory: -PARAMETRIC EXPRESSIONS ----------------------- -| 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 | +```python +from pathlib import Path -SIMULATION SETUP ----------------- -Analysis Types: Static (SOL 101), Modal (SOL 103) -Material: Aluminum 6061-T6 (E=68.9 GPa, ρ=2700 kg/m³) -Loads: - - Force: 500 N at tip - - 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. -``` +def save_introspection_report(study_dir: Path, report_content: str): + """Save MODEL_INTROSPECTION.md to study directory.""" + report_path = study_dir / "MODEL_INTROSPECTION.md" + with open(report_path, 'w') as f: + f.write(report_content) + print(f"Saved introspection report: {report_path}") ## Error Handling diff --git a/.claude/skills/core/study-creation-core.md b/.claude/skills/core/study-creation-core.md new file mode 100644 index 00000000..0af6a875 --- /dev/null +++ b/.claude/skills/core/study-creation-core.md @@ -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) diff --git a/.claude/skills/create-study-wizard.md b/.claude/skills/create-study-wizard.md new file mode 100644 index 00000000..460ec128 --- /dev/null +++ b/.claude/skills/create-study-wizard.md @@ -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` diff --git a/.claude/skills/create-study.md b/.claude/skills/create-study.md index 99904e55..46092836 100644 --- a/.claude/skills/create-study.md +++ b/.claude/skills/create-study.md @@ -1,7 +1,7 @@ # Create Optimization Study Skill -**Last Updated**: December 4, 2025 -**Version**: 2.1 - Added Mandatory Documentation Requirements +**Last Updated**: December 6, 2025 +**Version**: 2.2 - Added Model Introspection 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 **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, design variables, objectives, algorithm config | At study creation | | `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}/run_optimization.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}/STUDY_REPORT.md # MANDATORY - Results report template [✓] studies/{study_name}/NX_FILE_MODIFICATIONS_REQUIRED.md (if needed) diff --git a/.claude/skills/guided-study-wizard.md b/.claude/skills/guided-study-wizard.md new file mode 100644 index 00000000..9f0252b7 --- /dev/null +++ b/.claude/skills/guided-study-wizard.md @@ -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 diff --git a/.claude/skills/modules/extractors-catalog.md b/.claude/skills/modules/extractors-catalog.md new file mode 100644 index 00000000..465ff8f5 --- /dev/null +++ b/.claude/skills/modules/extractors-catalog.md @@ -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) diff --git a/.claude/skills/modules/neural-acceleration.md b/.claude/skills/modules/neural-acceleration.md new file mode 100644 index 00000000..a48b5ee1 --- /dev/null +++ b/.claude/skills/modules/neural-acceleration.md @@ -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) diff --git a/.claude/skills/modules/nx-docs-lookup.md b/.claude/skills/modules/nx-docs-lookup.md new file mode 100644 index 00000000..db24d768 --- /dev/null +++ b/.claude/skills/modules/nx-docs-lookup.md @@ -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* diff --git a/.claude/skills/modules/zernike-optimization.md b/.claude/skills/modules/zernike-optimization.md new file mode 100644 index 00000000..ca31f121 --- /dev/null +++ b/.claude/skills/modules/zernike-optimization.md @@ -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) diff --git a/CLAUDE.md b/CLAUDE.md index 6fac8734..5f812e93 100644 --- a/CLAUDE.md +++ b/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. +## 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 -**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: -- 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 +The Protocol Operating System (POS) provides layered documentation: -## 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 -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 +**Context loading rules**: See `.claude/skills/02_CONTEXT_LOADER.md` -### 2. Analyze NX Models -When user provides NX files: -- Extract expressions (design parameters) -- Identify simulation setup -- Suggest optimization targets -- Check for multi-solution requirements +## Task → Protocol Quick Lookup -### 3. Run & Monitor Optimizations -- Start optimization runs -- Check progress in databases -- Interpret results -- Generate reports +| Task | Protocol | Key File | +|------|----------|----------| +| Create study | OP_01 | `docs/protocols/operations/OP_01_CREATE_STUDY.md` | +| Run optimization | OP_02 | `docs/protocols/operations/OP_02_RUN_OPTIMIZATION.md` | +| 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 -When optimization needs >50 trials: -- Generate space-filling training data -- Run parallel FEA for training -- Train and validate surrogates -- Enable accelerated optimization +## System Protocols (Technical Specs) -### 5. Troubleshoot Issues -- Parse error logs -- Identify common problems -- Suggest fixes -- Recover from failures +| # | Name | When to Load | +|---|------|--------------| +| 10 | IMSO (Adaptive) | Single-objective, "adaptive", "intelligent" | +| 11 | Multi-Objective | 2+ objectives, "pareto", NSGA-II | +| 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 -**CRITICAL: Always use the `atomizer` conda environment.** All dependencies are pre-installed. +**CRITICAL: Always use the `atomizer` conda environment.** ```bash -# Activate before ANY Python command conda activate atomizer - -# Then run scripts -python run_optimization.py --start -python -m optimization_engine.runner ... +python run_optimization.py ``` **DO NOT:** -- Install packages with pip/conda (everything is already installed) +- Install packages with pip/conda (everything is installed) - Create new virtual environments - Use system Python -**Pre-installed packages include:** optuna, numpy, scipy, pandas, matplotlib, pyNastran, torch, plotly, and all Atomizer dependencies. - -## Key Files & Locations +## Key Directories ``` Atomizer/ -├── .claude/ -│ ├── skills/ # Skill instructions (READ THESE) -│ │ ├── create-study.md # Main study creation skill -│ │ └── analyze-workflow.md -│ └── settings.local.json -├── docs/ -│ ├── 01_PROTOCOLS.md # Quick protocol reference -│ ├── 06_PROTOCOLS_DETAILED/ # Full protocol docs -│ └── 07_DEVELOPMENT/ # Development plans +├── .claude/skills/ # LLM skills (Bootstrap + Core + Modules) +├── docs/protocols/ # Protocol Operating System +│ ├── operations/ # OP_01 - OP_06 +│ ├── system/ # SYS_10 - SYS_14 +│ └── extensions/ # EXT_01 - EXT_04 ├── optimization_engine/ # Core Python modules -│ ├── runner.py # Main optimizer -│ ├── nx_solver.py # NX interface -│ ├── 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 +│ └── extractors/ # Physics extraction library +├── studies/ # User studies └── atomizer-dashboard/ # React dashboard ``` -## Conversation Patterns +## CRITICAL: NX Open Development Protocol -### User: "I want to optimize this bracket" -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 +### Always Use Official Documentation First -### User: "Run 200 trials with neural network" -1. Check if surrogate_settings needed -2. Modify config to enable NN -3. Explain the hybrid workflow stages -4. Start run, show monitoring options +**For ANY development involving NX, NX Open, or Siemens APIs:** -### User: "What's the status?" -1. Query database for trial counts -2. Check for running background processes -3. Summarize progress and best results -4. Suggest next steps +1. **FIRST** - Query the MCP Siemens docs tools: + - `mcp__siemens-docs__nxopen_get_class` - Get class documentation + - `mcp__siemens-docs__nxopen_get_index` - Browse class/function indexes + - `mcp__siemens-docs__siemens_docs_list` - List available resources -### User: "The optimization failed" -1. Read error logs -2. Check common failure modes -3. Suggest fixes -4. Offer to retry +2. **THEN** - Use secondary sources if needed: + - PyNastran documentation (for BDF/OP2 parsing) + - NXOpen TSE examples in `nx_journals/` + - Existing extractors in `optimization_engine/extractors/` -## Protocols Reference +3. **NEVER** - Guess NX Open API calls without checking documentation first -| Protocol | Use Case | Sampler | -|----------|----------|---------| -| Protocol 10 | Single objective + constraints | TPE/CMA-ES | -| Protocol 11 | Multi-objective (2-3 goals) | NSGA-II | -| Protocol 12 | Hybrid FEA/NN acceleration | NSGA-II + surrogate | - -## Result Extraction - -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:** +**Available NX Open Classes (quick lookup):** +| Class | Page ID | Description | +|-------|---------|-------------| +| Session | a03318.html | Main NX session object | +| Part | a02434.html | Part file operations | +| BasePart | a00266.html | Base class for parts | +| CaeSession | a10510.html | CAE/FEM session | +| PdmSession | a50542.html | PDM integration | +**Example workflow for NX journal development:** ``` -□ Did I check optimization_engine/extractors/__init__.py? -□ Did I grep for similar function names in optimization_engine/? -□ Does this functionality exist somewhere else in the codebase? +1. User: "Extract mass from NX part" +2. Claude: Query nxopen_get_class("Part") to find mass-related methods +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 -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 -2. **SEARCH** - The functionality probably exists -3. **IMPORT** - Use the existing module -4. **Only if truly new** - Create in `optimization_engine/extractors/`, NOT in the study +2. **SEARCH** - Check `optimization_engine/extractors/` +3. **IMPORT** - Use existing extractor +4. **Only if truly new** - Follow EXT_01 to create new extractor -### Available Extractors (ALWAYS CHECK FIRST) +### Available Extractors -| Module | Functions | Use For | -|--------|-----------|---------| -| **`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 | -| **`zernike_helpers.py`** | `create_zernike_objective`, `ZernikeObjectiveBuilder`, `extract_zernike_for_trial` | Zernike optimization integration | -| **`extract_displacement.py`** | `extract_displacement` | Max/min displacement from OP2 | -| **`extract_von_mises_stress.py`** | `extract_solid_stress` | Von Mises stress extraction | -| **`extract_frequency.py`** | `extract_frequency` | Natural frequencies from OP2 | -| **`extract_mass.py`** | `extract_mass_from_expression` | CAD mass property | -| **`op2_extractor.py`** | Generic OP2 result extraction | Low-level OP2 access | -| **`field_data_extractor.py`** | Field data for neural networks | Training data generation | +| ID | Physics | Function | +|----|---------|----------| +| E1 | Displacement | `extract_displacement()` | +| E2 | Frequency | `extract_frequency()` | +| E3 | Stress | `extract_solid_stress()` | +| E4 | BDF Mass | `extract_mass_from_bdf()` | +| E5 | CAD Mass | `extract_mass_from_expression()` | +| E8-10 | Zernike | `extract_zernike_*()` | -### Correct Pattern: Zernike Example +**Full catalog**: `docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md` -**❌ WRONG - What I did (and must NEVER do again):** -```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 -``` +## Privilege Levels -**✅ CORRECT - What I should have done:** -```python -# studies/m1_mirror/run_optimization.py -from optimization_engine.extractors import ( - ZernikeExtractor, - extract_zernike_for_trial -) +| Level | Operations | Extensions | +|-------|------------|------------| +| **user** | All OP_* | None | +| **power_user** | All OP_* | EXT_01, EXT_02 | +| **admin** | All | All | -# In objective function - 5 lines instead of 500 -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 | +Default to `user` unless explicitly stated otherwise. ## Key Principles 1. **Conversation first** - Don't ask user to edit JSON manually 2. **Validate everything** - Catch errors before they cause failures 3. **Explain decisions** - Say why you chose a sampler/protocol -4. **Sensible defaults** - User only specifies what they care about -5. **Progressive disclosure** - Start simple, add complexity when needed -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. +4. **NEVER modify master files** - Copy NX files to study directory +5. **ALWAYS reuse code** - Check extractors before writing new code -## Current State Awareness +## CRITICAL: NX FEM Mesh Update Requirements -Check these before suggesting actions: -- Running background processes: `/tasks` command -- Study databases: `studies/*/2_results/study.db` -- Model files: `studies/*/1_setup/model/` -- Dashboard status: Check if servers running +**When parametric optimization produces identical results, the mesh is NOT updating!** + +### Required File Chain +``` +.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 -1. Read the relevant skill file -2. Check docs/06_PROTOCOLS_DETAILED/ -3. Look at existing similar studies +1. Check `.claude/skills/00_BOOTSTRAP.md` for task routing +2. Check `.claude/skills/01_CHEATSHEET.md` for quick lookup +3. Load relevant protocol from `docs/protocols/` 4. Ask user for clarification --- diff --git a/config.py b/config.py index 5358b34c..feb802f1 100644 --- a/config.py +++ b/config.py @@ -14,7 +14,7 @@ import os # NX Installation Directory # 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}") # Derived NX Paths (automatically updated when NX_VERSION changes) diff --git a/docs/protocols/README.md b/docs/protocols/README.md new file mode 100644 index 00000000..61a77a4c --- /dev/null +++ b/docs/protocols/README.md @@ -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 + + + +## 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 | diff --git a/docs/protocols/extensions/EXT_01_CREATE_EXTRACTOR.md b/docs/protocols/extensions/EXT_01_CREATE_EXTRACTOR.md new file mode 100644 index 00000000..f3782c4e --- /dev/null +++ b/docs/protocols/extensions/EXT_01_CREATE_EXTRACTOR.md @@ -0,0 +1,395 @@ +# EXT_01: Create New Extractor + + + +## 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 | diff --git a/docs/protocols/extensions/EXT_02_CREATE_HOOK.md b/docs/protocols/extensions/EXT_02_CREATE_HOOK.md new file mode 100644 index 00000000..99236dec --- /dev/null +++ b/docs/protocols/extensions/EXT_02_CREATE_HOOK.md @@ -0,0 +1,366 @@ +# EXT_02: Create Lifecycle Hook + + + +## 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 | diff --git a/docs/protocols/extensions/EXT_03_CREATE_PROTOCOL.md b/docs/protocols/extensions/EXT_03_CREATE_PROTOCOL.md new file mode 100644 index 00000000..55e61677 --- /dev/null +++ b/docs/protocols/extensions/EXT_03_CREATE_PROTOCOL.md @@ -0,0 +1,263 @@ +# EXT_03: Create New Protocol + + + +## 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 + + + +## 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 + +``` + +### 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 | diff --git a/docs/protocols/extensions/EXT_04_CREATE_SKILL.md b/docs/protocols/extensions/EXT_04_CREATE_SKILL.md new file mode 100644 index 00000000..fdfa44dc --- /dev/null +++ b/docs/protocols/extensions/EXT_04_CREATE_SKILL.md @@ -0,0 +1,331 @@ +# EXT_04: Create New Skill + + + +## 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 | diff --git a/docs/protocols/extensions/templates/extractor_template.py b/docs/protocols/extensions/templates/extractor_template.py new file mode 100644 index 00000000..673c4211 --- /dev/null +++ b/docs/protocols/extensions/templates/extractor_template.py @@ -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 +# ========================================= diff --git a/docs/protocols/extensions/templates/hook_template.py b/docs/protocols/extensions/templates/hook_template.py new file mode 100644 index 00000000..74c4c5fa --- /dev/null +++ b/docs/protocols/extensions/templates/hook_template.py @@ -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} diff --git a/docs/protocols/extensions/templates/protocol_template.md b/docs/protocols/extensions/templates/protocol_template.md new file mode 100644 index 00000000..c9eef64f --- /dev/null +++ b/docs/protocols/extensions/templates/protocol_template.md @@ -0,0 +1,112 @@ +# {LAYER}_{NUMBER}_{NAME} + + + +## 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 | diff --git a/docs/protocols/operations/OP_01_CREATE_STUDY.md b/docs/protocols/operations/OP_01_CREATE_STUDY.md new file mode 100644 index 00000000..40e4041e --- /dev/null +++ b/docs/protocols/operations/OP_01_CREATE_STUDY.md @@ -0,0 +1,403 @@ +# OP_01: Create Optimization Study + + + +## 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 | diff --git a/docs/protocols/operations/OP_02_RUN_OPTIMIZATION.md b/docs/protocols/operations/OP_02_RUN_OPTIMIZATION.md new file mode 100644 index 00000000..5d97358d --- /dev/null +++ b/docs/protocols/operations/OP_02_RUN_OPTIMIZATION.md @@ -0,0 +1,297 @@ +# OP_02: Run Optimization + + + +## 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 | diff --git a/docs/protocols/operations/OP_03_MONITOR_PROGRESS.md b/docs/protocols/operations/OP_03_MONITOR_PROGRESS.md new file mode 100644 index 00000000..ee416233 --- /dev/null +++ b/docs/protocols/operations/OP_03_MONITOR_PROGRESS.md @@ -0,0 +1,246 @@ +# OP_03: Monitor Progress + + + +## 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 | diff --git a/docs/protocols/operations/OP_04_ANALYZE_RESULTS.md b/docs/protocols/operations/OP_04_ANALYZE_RESULTS.md new file mode 100644 index 00000000..cf7fa38a --- /dev/null +++ b/docs/protocols/operations/OP_04_ANALYZE_RESULTS.md @@ -0,0 +1,302 @@ +# OP_04: Analyze Results + + + +## 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 | diff --git a/docs/protocols/operations/OP_05_EXPORT_TRAINING_DATA.md b/docs/protocols/operations/OP_05_EXPORT_TRAINING_DATA.md new file mode 100644 index 00000000..3f9fffc8 --- /dev/null +++ b/docs/protocols/operations/OP_05_EXPORT_TRAINING_DATA.md @@ -0,0 +1,294 @@ +# OP_05: Export Training Data + + + +## 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 | diff --git a/docs/protocols/operations/OP_06_TROUBLESHOOT.md b/docs/protocols/operations/OP_06_TROUBLESHOOT.md new file mode 100644 index 00000000..48c5dc65 --- /dev/null +++ b/docs/protocols/operations/OP_06_TROUBLESHOOT.md @@ -0,0 +1,437 @@ +# OP_06: Troubleshoot + + + +## 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 | diff --git a/docs/protocols/system/SYS_10_IMSO.md b/docs/protocols/system/SYS_10_IMSO.md new file mode 100644 index 00000000..854f145a --- /dev/null +++ b/docs/protocols/system/SYS_10_IMSO.md @@ -0,0 +1,341 @@ +# SYS_10: Intelligent Multi-Strategy Optimization (IMSO) + + + +## 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%. diff --git a/docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md b/docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md new file mode 100644 index 00000000..1a555e6f --- /dev/null +++ b/docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md @@ -0,0 +1,338 @@ +# SYS_11: Multi-Objective Support + + + +## 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 | diff --git a/docs/protocols/system/SYS_13_DASHBOARD_TRACKING.md b/docs/protocols/system/SYS_13_DASHBOARD_TRACKING.md new file mode 100644 index 00000000..c233ad11 --- /dev/null +++ b/docs/protocols/system/SYS_13_DASHBOARD_TRACKING.md @@ -0,0 +1,435 @@ +# SYS_13: Real-Time Dashboard Tracking + + + +## 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(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 ( + +
Phase: {state?.current_phase}
+
Strategy: {state?.current_strategy}
+ +
+ ); +} +``` + +### 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 | diff --git a/docs/protocols/system/SYS_14_NEURAL_ACCELERATION.md b/docs/protocols/system/SYS_14_NEURAL_ACCELERATION.md new file mode 100644 index 00000000..374008c5 --- /dev/null +++ b/docs/protocols/system/SYS_14_NEURAL_ACCELERATION.md @@ -0,0 +1,564 @@ +# SYS_14: Neural Network Acceleration + + + +## 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 | diff --git a/optimization_engine/hooks/README.md b/optimization_engine/hooks/README.md new file mode 100644 index 00000000..44abfd57 --- /dev/null +++ b/optimization_engine/hooks/README.md @@ -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 diff --git a/optimization_engine/hooks/__init__.py b/optimization_engine/hooks/__init__.py new file mode 100644 index 00000000..2e336deb --- /dev/null +++ b/optimization_engine/hooks/__init__.py @@ -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' diff --git a/optimization_engine/hooks/examples.py b/optimization_engine/hooks/examples.py new file mode 100644 index 00000000..b0cd2f44 --- /dev/null +++ b/optimization_engine/hooks/examples.py @@ -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() diff --git a/optimization_engine/hooks/nx_cad/__init__.py b/optimization_engine/hooks/nx_cad/__init__.py new file mode 100644 index 00000000..9a4c4daf --- /dev/null +++ b/optimization_engine/hooks/nx_cad/__init__.py @@ -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', +] diff --git a/optimization_engine/hooks/nx_cad/expression_manager.py b/optimization_engine/hooks/nx_cad/expression_manager.py new file mode 100644 index 00000000..4fa196e1 --- /dev/null +++ b/optimization_engine/hooks/nx_cad/expression_manager.py @@ -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 [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] + ) diff --git a/optimization_engine/hooks/nx_cad/feature_manager.py b/optimization_engine/hooks/nx_cad/feature_manager.py new file mode 100644 index 00000000..487ace6c --- /dev/null +++ b/optimization_engine/hooks/nx_cad/feature_manager.py @@ -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 [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]) diff --git a/optimization_engine/hooks/nx_cad/geometry_query.py b/optimization_engine/hooks/nx_cad/geometry_query.py new file mode 100644 index 00000000..869b73a5 --- /dev/null +++ b/optimization_engine/hooks/nx_cad/geometry_query.py @@ -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 [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) diff --git a/optimization_engine/hooks/nx_cad/model_introspection.py b/optimization_engine/hooks/nx_cad/model_introspection.py new file mode 100644 index 00000000..e1f3741e --- /dev/null +++ b/optimization_engine/hooks/nx_cad/model_introspection.py @@ -0,0 +1,1158 @@ +""" +NX Model Introspection Hook +=========================== + +Comprehensive extraction of ALL model information from NX parts and simulations. +Provides a complete "big picture" view of what's available for optimization. + +Features: +- Part introspection: expressions, mass, material, bodies, features +- Simulation introspection: solutions, BCs, loads, output requests +- OP2 introspection: available results (displacement, stress, strain, etc.) + +Usage: + from optimization_engine.hooks.nx_cad import model_introspection + + # Get everything from a part + result = model_introspection.introspect_part("C:/model.prt") + + # Get everything from a simulation + result = model_introspection.introspect_simulation("C:/model.sim") + + # Get everything from an OP2 results file + result = model_introspection.introspect_op2("C:/results.op2") + +Phase 2.5 - NX Open Automation Roadmap +""" + +import os +import json +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, Dict, Any, List + +# NX installation path (configurable) +NX_BIN_PATH = os.environ.get( + "NX_BIN_PATH", + r"C:\Program Files\Siemens\NX2506\NXBIN" +) + + +# ============================================================================= +# NX Journal for PART Introspection +# ============================================================================= +PART_INTROSPECTION_JOURNAL = ''' +# NX Open Python Journal - Part Introspection +# Extracts ALL information from an NX part file +# Auto-generated by Atomizer hooks + +import NXOpen +import NXOpen.UF +import json +import sys +import os + +def main(): + """Extract comprehensive part information.""" + session = NXOpen.Session.GetSession() + + args = sys.argv[1:] if len(sys.argv) > 1 else [] + if len(args) < 2: + raise ValueError("Usage: script.py ") + + part_path = args[0] + output_json = args[1] + + result = {"success": False, "error": None, "data": {}} + + try: + part = ensure_part_open(session, part_path) + if part is None: + result["error"] = f"Failed to open part: {part_path}" + else: + result = introspect_part(session, part, part_path) + except Exception as e: + import traceback + result["error"] = str(e) + result["traceback"] = traceback.format_exc() + + with open(output_json, 'w') as f: + json.dump(result, f, indent=2) + + return result + + +def ensure_part_open(session, part_path): + """Open part if not already open.""" + part_path_norm = os.path.normpath(part_path).lower() + + for part in session.Parts: + if os.path.normpath(part.FullPath).lower() == part_path_norm: + return part + + if not os.path.exists(part_path): + return None + + try: + working_dir = os.path.dirname(part_path) + session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory + session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True]) + + part, load_status = session.Parts.OpenActiveDisplay( + part_path, NXOpen.DisplayPartOption.AllowAdditional + ) + load_status.Dispose() + return part + except: + return None + + +def introspect_part(session, part, part_path): + """Extract all information from a part.""" + result = {"success": True, "error": None, "data": {}} + + # Basic info + result["data"]["file_info"] = { + "path": part_path, + "name": part.Name, + "leaf": part.Leaf, + "is_modified": part.IsModified, + } + + # Expressions + result["data"]["expressions"] = extract_expressions(part) + + # Bodies + result["data"]["bodies"] = extract_bodies(part) + + # Mass properties + result["data"]["mass_properties"] = extract_mass_properties(part) + + # Features + result["data"]["features"] = extract_features(part) + + # Attributes + result["data"]["attributes"] = extract_attributes(part) + + # Units + result["data"]["units"] = extract_units(part) + + # Summary + result["data"]["summary"] = { + "expression_count": result["data"]["expressions"]["count"], + "user_expression_count": len([e for e in result["data"]["expressions"].get("expressions", {}).values() + if not e.get("name", "").startswith("p")]), + "body_count": result["data"]["bodies"]["count"], + "solid_body_count": result["data"]["bodies"]["solid_count"], + "feature_count": result["data"]["features"]["count"], + "has_mass": result["data"]["mass_properties"].get("mass") is not None, + } + + return result + + +def extract_expressions(part): + """Extract all expressions from the part.""" + data = {"count": 0, "expressions": {}, "by_type": {}} + + try: + for expr in part.Expressions: + try: + expr_type = str(expr.Type) if hasattr(expr, 'Type') else "Unknown" + expr_data = { + "name": expr.Name, + "value": expr.Value, + "rhs": expr.RightHandSide, + "units": expr.Units.Name if expr.Units else None, + "type": expr_type, + "is_geometric": "p" in expr.Name.lower()[:2], # p0, p1, etc. are internal + } + data["expressions"][expr.Name] = expr_data + + # Group by type + if expr_type not in data["by_type"]: + data["by_type"][expr_type] = [] + data["by_type"][expr_type].append(expr.Name) + + except: + pass + + data["count"] = len(data["expressions"]) + except Exception as e: + data["error"] = str(e) + + return data + + +def extract_bodies(part): + """Extract body information.""" + data = {"count": 0, "solid_count": 0, "sheet_count": 0, "bodies": []} + + try: + for body in part.Bodies: + body_data = { + "name": body.Name if hasattr(body, 'Name') else None, + "is_solid": body.IsSolidBody, + "is_sheet": body.IsSheetBody, + "material": None, + } + + try: + phys_mat = body.GetPhysicalMaterial() + if phys_mat: + body_data["material"] = phys_mat.Name + except: + pass + + data["bodies"].append(body_data) + data["count"] += 1 + if body.IsSolidBody: + data["solid_count"] += 1 + if body.IsSheetBody: + data["sheet_count"] += 1 + + except Exception as e: + data["error"] = str(e) + + return data + + +def extract_mass_properties(part): + """Extract mass properties.""" + data = {} + + try: + solid_bodies = [b for b in part.Bodies if b.IsSolidBody] + if not solid_bodies: + data["error"] = "No solid bodies" + return data + + measure_manager = part.MeasureManager + uc = part.UnitCollection + mass_units = [ + uc.GetBase("Area"), + uc.GetBase("Volume"), + uc.GetBase("Mass"), + uc.GetBase("Length") + ] + + mass_props = measure_manager.NewMassProperties(mass_units, 0.99, solid_bodies) + + data["mass"] = mass_props.Mass + data["mass_unit"] = "kg" + data["volume"] = mass_props.Volume + data["volume_unit"] = "mm^3" + data["surface_area"] = mass_props.Area + data["area_unit"] = "mm^2" + + centroid = mass_props.Centroid + data["centroid"] = { + "x": centroid.X, + "y": centroid.Y, + "z": centroid.Z, + "unit": "mm" + } + + try: + pm = mass_props.PrincipalMomentsOfInertia + data["principal_moments"] = { + "Ixx": pm[0], "Iyy": pm[1], "Izz": pm[2], + "unit": "kg*mm^2" + } + except: + pass + + except Exception as e: + data["error"] = str(e) + + return data + + +def extract_features(part): + """Extract feature information.""" + data = {"count": 0, "features": [], "by_type": {}} + + try: + for feat in part.Features: + try: + feat_type = feat.GetFeatureName() if hasattr(feat, 'GetFeatureName') else type(feat).__name__ + feat_data = { + "name": feat.Name if hasattr(feat, 'Name') else None, + "type": feat_type, + "suppressed": feat.IsSuppressed if hasattr(feat, 'IsSuppressed') else False, + } + data["features"].append(feat_data) + + if feat_type not in data["by_type"]: + data["by_type"][feat_type] = 0 + data["by_type"][feat_type] += 1 + + except: + pass + + data["count"] = len(data["features"]) + except Exception as e: + data["error"] = str(e) + + return data + + +def extract_attributes(part): + """Extract part attributes.""" + data = {"count": 0, "attributes": {}} + + try: + attrs = part.GetUserAttributes() + for attr in attrs: + try: + attr_data = { + "title": attr.Title, + "type": str(attr.Type), + } + if hasattr(attr, 'StringValue'): + attr_data["value"] = attr.StringValue + elif hasattr(attr, 'RealValue'): + attr_data["value"] = attr.RealValue + elif hasattr(attr, 'IntegerValue'): + attr_data["value"] = attr.IntegerValue + + data["attributes"][attr.Title] = attr_data + except: + pass + + data["count"] = len(data["attributes"]) + except Exception as e: + data["error"] = str(e) + + return data + + +def extract_units(part): + """Extract unit system information.""" + data = {} + + try: + uc = part.UnitCollection + data["system"] = "metric" # NX default + + # Get base units + data["base_units"] = {} + for unit_type in ["Length", "Mass", "Time", "Area", "Volume"]: + try: + unit = uc.GetBase(unit_type) + data["base_units"][unit_type] = unit.Name if unit else None + except: + pass + + except Exception as e: + data["error"] = str(e) + + return data + + +if __name__ == "__main__": + main() +''' + + +# ============================================================================= +# NX Journal for SIMULATION Introspection +# ============================================================================= +SIMULATION_INTROSPECTION_JOURNAL = ''' +# NX Open Python Journal - Simulation Introspection +# Extracts ALL information from an NX simulation file +# Auto-generated by Atomizer hooks + +import NXOpen +import NXOpen.CAE +import json +import sys +import os + +def main(): + """Extract comprehensive simulation information.""" + session = NXOpen.Session.GetSession() + + args = sys.argv[1:] if len(sys.argv) > 1 else [] + if len(args) < 2: + raise ValueError("Usage: script.py ") + + sim_path = args[0] + output_json = args[1] + + result = {"success": False, "error": None, "data": {}} + + try: + # 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 simulation + basePart, loadStatus = session.Parts.OpenActiveDisplay( + sim_path, NXOpen.DisplayPartOption.AllowAdditional + ) + loadStatus.Dispose() + + simPart = session.Parts.Work + + if not isinstance(simPart, NXOpen.CAE.SimPart): + result["error"] = f"Not a simulation part: {type(simPart)}" + else: + result = introspect_simulation(session, simPart, sim_path) + + except Exception as e: + import traceback + result["error"] = str(e) + result["traceback"] = traceback.format_exc() + + with open(output_json, 'w') as f: + json.dump(result, f, indent=2) + + return result + + +def introspect_simulation(session, simPart, sim_path): + """Extract all information from a simulation.""" + result = {"success": True, "error": None, "data": {}} + + sim = simPart.Simulation + + # Basic info + result["data"]["file_info"] = { + "path": sim_path, + "name": simPart.Name, + "simulation_name": sim.Name if sim else None, + } + + # Solutions + result["data"]["solutions"] = extract_solutions(sim) + + # Boundary Conditions + result["data"]["boundary_conditions"] = extract_boundary_conditions(sim) + + # Loads + result["data"]["loads"] = extract_loads(sim) + + # Materials + result["data"]["materials"] = extract_sim_materials(simPart) + + # Mesh info + result["data"]["mesh"] = extract_mesh_info(simPart) + + # Output requests (from first solution) + result["data"]["output_requests"] = extract_output_requests(sim) + + # Summary + result["data"]["summary"] = { + "solution_count": len(result["data"]["solutions"].get("solutions", [])), + "bc_count": result["data"]["boundary_conditions"].get("count", 0), + "load_count": result["data"]["loads"].get("count", 0), + "material_count": result["data"]["materials"].get("count", 0), + "node_count": result["data"]["mesh"].get("node_count", 0), + "element_count": result["data"]["mesh"].get("element_count", 0), + } + + return result + + +def extract_solutions(sim): + """Extract solution information.""" + data = {"solutions": []} + + try: + for sol in sim.Solutions: + sol_data = { + "name": sol.Name, + "type": str(sol.SolutionType) if hasattr(sol, 'SolutionType') else None, + "solver": None, + } + + # Try to get solver type + try: + sol_data["solver"] = sol.SolverTypeName if hasattr(sol, 'SolverTypeName') else "Nastran" + except: + sol_data["solver"] = "Nastran" + + # Try to get analysis type + try: + sol_data["analysis_type"] = str(sol.AnalysisType) if hasattr(sol, 'AnalysisType') else None + except: + pass + + data["solutions"].append(sol_data) + + except Exception as e: + data["error"] = str(e) + + return data + + +def extract_boundary_conditions(sim): + """Extract boundary condition information.""" + data = {"count": 0, "boundary_conditions": [], "by_type": {}} + + try: + for bc in sim.BoundaryConditions: + try: + bc_type = type(bc).__name__ + bc_data = { + "name": bc.Name if hasattr(bc, 'Name') else None, + "type": bc_type, + "active": bc.IsActive if hasattr(bc, 'IsActive') else True, + } + + # Try to get constraint type details + if hasattr(bc, 'ConstraintType'): + bc_data["constraint_type"] = str(bc.ConstraintType) + + # Check for SPC (fixed constraints) + if 'SPC' in bc_type or 'Constraint' in bc_type or 'Fixed' in bc_type: + bc_data["category"] = "constraint" + else: + bc_data["category"] = "other" + + data["boundary_conditions"].append(bc_data) + + if bc_type not in data["by_type"]: + data["by_type"][bc_type] = 0 + data["by_type"][bc_type] += 1 + + except: + pass + + data["count"] = len(data["boundary_conditions"]) + except Exception as e: + data["error"] = str(e) + + return data + + +def extract_loads(sim): + """Extract load information.""" + data = {"count": 0, "loads": [], "by_type": {}} + + try: + for load in sim.Loads: + try: + load_type = type(load).__name__ + load_data = { + "name": load.Name if hasattr(load, 'Name') else None, + "type": load_type, + "active": load.IsActive if hasattr(load, 'IsActive') else True, + } + + # Try to get load magnitude + if hasattr(load, 'Magnitude'): + try: + load_data["magnitude"] = load.Magnitude.Value + load_data["magnitude_unit"] = load.Magnitude.Units.Name if load.Magnitude.Units else None + except: + pass + + # Categorize load type + if 'Force' in load_type: + load_data["category"] = "force" + elif 'Pressure' in load_type: + load_data["category"] = "pressure" + elif 'Moment' in load_type or 'Torque' in load_type: + load_data["category"] = "moment" + elif 'Temperature' in load_type or 'Thermal' in load_type: + load_data["category"] = "thermal" + elif 'Gravity' in load_type or 'Acceleration' in load_type: + load_data["category"] = "inertia" + else: + load_data["category"] = "other" + + data["loads"].append(load_data) + + if load_type not in data["by_type"]: + data["by_type"][load_type] = 0 + data["by_type"][load_type] += 1 + + except: + pass + + data["count"] = len(data["loads"]) + except Exception as e: + data["error"] = str(e) + + return data + + +def extract_sim_materials(simPart): + """Extract material information from simulation.""" + data = {"count": 0, "materials": []} + + try: + # Try to get materials from the simulation + for mat in simPart.MaterialManager.PhysicalMaterials: + try: + mat_data = { + "name": mat.Name, + "properties": {} + } + + # Common material properties + for prop_name in ["Density", "YoungsModulus", "PoissonsRatio", + "ThermalExpansionCoefficient", "ThermalConductivity"]: + try: + val = mat.GetRealPropertyValue(prop_name) + mat_data["properties"][prop_name] = val + except: + pass + + data["materials"].append(mat_data) + except: + pass + + data["count"] = len(data["materials"]) + except Exception as e: + data["error"] = str(e) + + return data + + +def extract_mesh_info(simPart): + """Extract mesh information.""" + data = {"node_count": 0, "element_count": 0, "element_types": {}} + + try: + # Try to get mesh from FEM + fem = None + try: + # For assembly FEM, need to navigate to component + fem = simPart.FemPart.BaseFEModel if hasattr(simPart, 'FemPart') else None + except: + pass + + if fem is None: + try: + # Direct access + fem = simPart.Simulation.Femodel + except: + pass + + if fem: + try: + data["node_count"] = fem.MeshManager.NodeCount if hasattr(fem.MeshManager, 'NodeCount') else 0 + data["element_count"] = fem.MeshManager.ElementCount if hasattr(fem.MeshManager, 'ElementCount') else 0 + except: + pass + + except Exception as e: + data["error"] = str(e) + + return data + + +def extract_output_requests(sim): + """Extract output request information (what will be in the OP2).""" + data = {"requests": [], "available_outputs": []} + + try: + # Standard Nastran outputs that might be requested + standard_outputs = [ + {"name": "DISPLACEMENT", "code": "DISP", "description": "Nodal displacements"}, + {"name": "STRESS", "code": "STRESS", "description": "Element stresses"}, + {"name": "STRAIN", "code": "STRAIN", "description": "Element strains"}, + {"name": "FORCE", "code": "FORCE", "description": "Element forces"}, + {"name": "SPCFORCES", "code": "SPCF", "description": "Reaction forces at constraints"}, + {"name": "MPCFORCES", "code": "MPCF", "description": "Multi-point constraint forces"}, + {"name": "OLOAD", "code": "OLOAD", "description": "Applied loads"}, + {"name": "GPFORCE", "code": "GPFO", "description": "Grid point forces"}, + {"name": "ESE", "code": "ESE", "description": "Element strain energy"}, + {"name": "GPSTRESS", "code": "GPST", "description": "Grid point stresses"}, + {"name": "TEMPERATURE", "code": "TEMP", "description": "Nodal temperatures"}, + {"name": "VELOCITY", "code": "VELO", "description": "Nodal velocities"}, + {"name": "ACCELERATION", "code": "ACCE", "description": "Nodal accelerations"}, + {"name": "EIGENVECTOR", "code": "EIGV", "description": "Mode shapes"}, + ] + data["available_outputs"] = standard_outputs + + # Try to get actual output requests from solution + for sol in sim.Solutions: + try: + # Output requests are typically in case control + req_data = { + "solution": sol.Name, + "outputs": [] + } + + # Check what's typically output + req_data["outputs"] = ["DISPLACEMENT", "STRESS", "SPCFORCES"] # Default + data["requests"].append(req_data) + except: + pass + + except Exception as e: + data["error"] = str(e) + + return data + + +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=180 # 3 minute timeout for introspection + ) + + # 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[-1000:]}, stderr: {result.stderr[-500:]}", + "data": {} + } + except subprocess.TimeoutExpired: + return { + "success": False, + "error": "Journal execution timed out after 180 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 + + +# ============================================================================= +# OP2 Introspection (Pure Python using pyNastran) +# ============================================================================= +def introspect_op2(op2_path: str) -> Dict[str, Any]: + """ + Introspect an OP2 results file to see what data is available. + + Args: + op2_path: Path to OP2 file + + Returns: + Dict with available results, subcases, element types, etc. + """ + result = {"success": False, "error": None, "data": {}} + + if not os.path.exists(op2_path): + result["error"] = f"OP2 file not found: {op2_path}" + return result + + try: + from pyNastran.op2.op2 import OP2 + import logging + logging.getLogger('pyNastran').setLevel(logging.ERROR) + + op2 = OP2(debug=False, log=None) + op2.read_op2(op2_path) + + data = { + "file_info": { + "path": op2_path, + "size_mb": os.path.getsize(op2_path) / (1024 * 1024), + }, + "subcases": [], + "results": { + "displacement": {"available": False, "subcases": []}, + "velocity": {"available": False, "subcases": []}, + "acceleration": {"available": False, "subcases": []}, + "eigenvectors": {"available": False, "modes": 0}, + "spc_forces": {"available": False, "subcases": []}, + "mpc_forces": {"available": False, "subcases": []}, + "stress": {"available": False, "element_types": [], "subcases": []}, + "strain": {"available": False, "element_types": [], "subcases": []}, + "strain_energy": {"available": False, "subcases": []}, + "temperature": {"available": False, "subcases": []}, + "element_forces": {"available": False, "element_types": [], "subcases": []}, + }, + "mesh": { + "node_count": 0, + "element_count": 0, + "element_types": {}, + }, + "materials": [], + "properties": [], + } + + # Get subcases + subcases = set() + + # Check displacements + if hasattr(op2, 'displacements') and op2.displacements: + data["results"]["displacement"]["available"] = True + for sc in op2.displacements.keys(): + subcases.add(sc) + data["results"]["displacement"]["subcases"].append(sc) + + # Check velocities + if hasattr(op2, 'velocities') and op2.velocities: + data["results"]["velocity"]["available"] = True + for sc in op2.velocities.keys(): + subcases.add(sc) + data["results"]["velocity"]["subcases"].append(sc) + + # Check accelerations + if hasattr(op2, 'accelerations') and op2.accelerations: + data["results"]["acceleration"]["available"] = True + for sc in op2.accelerations.keys(): + subcases.add(sc) + data["results"]["acceleration"]["subcases"].append(sc) + + # Check eigenvectors + if hasattr(op2, 'eigenvectors') and op2.eigenvectors: + data["results"]["eigenvectors"]["available"] = True + for sc, eigv in op2.eigenvectors.items(): + data["results"]["eigenvectors"]["modes"] = len(eigv.modes) if hasattr(eigv, 'modes') else 0 + + # Check SPC forces + if hasattr(op2, 'spc_forces') and op2.spc_forces: + data["results"]["spc_forces"]["available"] = True + for sc in op2.spc_forces.keys(): + subcases.add(sc) + data["results"]["spc_forces"]["subcases"].append(sc) + + # Check MPC forces + if hasattr(op2, 'mpc_forces') and op2.mpc_forces: + data["results"]["mpc_forces"]["available"] = True + for sc in op2.mpc_forces.keys(): + subcases.add(sc) + data["results"]["mpc_forces"]["subcases"].append(sc) + + # Check stresses (various element types) + stress_attrs = [ + 'ctetra_stress', 'chexa_stress', 'cpenta_stress', 'cpyram_stress', + 'cquad4_stress', 'cquad8_stress', 'ctria3_stress', 'ctria6_stress', + 'cbar_stress', 'cbeam_stress', 'crod_stress', 'ctube_stress', + ] + for attr in stress_attrs: + if hasattr(op2, attr) and getattr(op2, attr): + stress_dict = getattr(op2, attr) + elem_type = attr.replace('_stress', '').upper() + data["results"]["stress"]["available"] = True + data["results"]["stress"]["element_types"].append(elem_type) + for sc in stress_dict.keys(): + subcases.add(sc) + if sc not in data["results"]["stress"]["subcases"]: + data["results"]["stress"]["subcases"].append(sc) + + # Check strains + strain_attrs = [attr.replace('_stress', '_strain') for attr in stress_attrs] + for attr in strain_attrs: + if hasattr(op2, attr) and getattr(op2, attr): + strain_dict = getattr(op2, attr) + elem_type = attr.replace('_strain', '').upper() + data["results"]["strain"]["available"] = True + data["results"]["strain"]["element_types"].append(elem_type) + for sc in strain_dict.keys(): + subcases.add(sc) + if sc not in data["results"]["strain"]["subcases"]: + data["results"]["strain"]["subcases"].append(sc) + + # Check strain energy + if hasattr(op2, 'ctetra_strain_energy') or hasattr(op2, 'strain_energy'): + se_attrs = ['ctetra_strain_energy', 'chexa_strain_energy', 'strain_energy'] + for attr in se_attrs: + if hasattr(op2, attr) and getattr(op2, attr): + data["results"]["strain_energy"]["available"] = True + se_dict = getattr(op2, attr) + for sc in se_dict.keys(): + subcases.add(sc) + if sc not in data["results"]["strain_energy"]["subcases"]: + data["results"]["strain_energy"]["subcases"].append(sc) + + # Check temperatures + if hasattr(op2, 'temperatures') and op2.temperatures: + data["results"]["temperature"]["available"] = True + for sc in op2.temperatures.keys(): + subcases.add(sc) + data["results"]["temperature"]["subcases"].append(sc) + + # Get mesh info from geometry tables + if hasattr(op2, 'nodes') and op2.nodes: + data["mesh"]["node_count"] = len(op2.nodes) + + # Count elements + elem_count = 0 + elem_attrs = ['elements', 'ctetra', 'chexa', 'cpenta', 'cquad4', 'ctria3'] + for attr in elem_attrs: + if hasattr(op2, attr): + elem_dict = getattr(op2, attr) + if elem_dict: + if isinstance(elem_dict, dict): + for etype, elems in elem_dict.items(): + count = len(elems) if hasattr(elems, '__len__') else 1 + data["mesh"]["element_types"][etype] = count + elem_count += count + data["mesh"]["element_count"] = elem_count + + data["subcases"] = sorted(list(subcases)) + + # Summary of what's extractable + data["extractable"] = [] + for result_type, info in data["results"].items(): + if info["available"]: + data["extractable"].append(result_type) + + result["success"] = True + result["data"] = data + + except ImportError: + result["error"] = "pyNastran not installed. Run: pip install pyNastran" + except Exception as e: + import traceback + result["error"] = str(e) + result["traceback"] = traceback.format_exc() + + return result + + +# ============================================================================= +# Public API +# ============================================================================= + +def introspect_part(part_path: str) -> Dict[str, Any]: + """ + Comprehensive introspection of an NX part file. + + Extracts: + - File info (name, path, modification status) + - All expressions (with values, units, types) + - Body information (solid/sheet, materials) + - Mass properties (mass, volume, centroid, inertia) + - Features (with types, suppression status) + - Attributes + - Unit system + + Args: + part_path: Path to .prt file + + Returns: + Dict with comprehensive part information + + Example: + >>> result = introspect_part("C:/models/bracket.prt") + >>> if result["success"]: + ... print(f"Expressions: {result['data']['summary']['expression_count']}") + ... print(f"Mass: {result['data']['mass_properties']['mass']} kg") + """ + part_path = str(Path(part_path).resolve()) + + if not os.path.exists(part_path): + return { + "success": False, + "error": f"Part file not found: {part_path}", + "data": {} + } + + return _run_journal(PART_INTROSPECTION_JOURNAL, part_path) + + +def introspect_simulation(sim_path: str) -> Dict[str, Any]: + """ + Comprehensive introspection of an NX simulation file. + + Extracts: + - File info + - All solutions (with types, solvers) + - Boundary conditions (constraints, with types) + - Loads (forces, pressures, moments, etc.) + - Materials (with properties) + - Mesh info (node/element counts) + - Output requests (what will be in results) + + Args: + sim_path: Path to .sim file + + Returns: + Dict with comprehensive simulation information + + Example: + >>> result = introspect_simulation("C:/models/bracket.sim") + >>> if result["success"]: + ... print(f"Solutions: {result['data']['summary']['solution_count']}") + ... print(f"Loads: {result['data']['summary']['load_count']}") + """ + sim_path = str(Path(sim_path).resolve()) + + if not os.path.exists(sim_path): + return { + "success": False, + "error": f"Simulation file not found: {sim_path}", + "data": {} + } + + return _run_journal(SIMULATION_INTROSPECTION_JOURNAL, sim_path) + + +def introspect_model(model_path: str) -> Dict[str, Any]: + """ + Unified introspection that detects file type and extracts all information. + + Automatically detects: + - .prt files -> Part introspection + - .sim files -> Simulation introspection + - .op2 files -> Results introspection + + Args: + model_path: Path to any NX file (.prt, .sim, .op2) + + Returns: + Dict with comprehensive model information + """ + model_path = str(Path(model_path).resolve()) + ext = Path(model_path).suffix.lower() + + if ext == '.prt': + return introspect_part(model_path) + elif ext == '.sim': + return introspect_simulation(model_path) + elif ext == '.op2': + return introspect_op2(model_path) + else: + return { + "success": False, + "error": f"Unsupported file type: {ext}. Supported: .prt, .sim, .op2", + "data": {} + } + + +def introspect_study(study_dir: str) -> Dict[str, Any]: + """ + Introspect an entire Atomizer study directory. + + Finds and extracts information from: + - All .prt files (CAD models) + - All .sim files (simulations) + - All .op2 files (results) + - optimization_config.json (if exists) + + Args: + study_dir: Path to study directory (e.g., studies/bracket_optimization/) + + Returns: + Dict with comprehensive study information + """ + study_dir = Path(study_dir) + + if not study_dir.exists(): + return { + "success": False, + "error": f"Study directory not found: {study_dir}", + "data": {} + } + + result = { + "success": True, + "error": None, + "data": { + "study_path": str(study_dir), + "parts": [], + "simulations": [], + "results": [], + "config": None, + } + } + + # Find all files + setup_dir = study_dir / "1_setup" + model_dir = setup_dir / "model" if setup_dir.exists() else study_dir + results_dir = study_dir / "2_results" if (study_dir / "2_results").exists() else study_dir + + # Find parts + prt_files = list(model_dir.glob("*.prt")) if model_dir.exists() else [] + for prt in prt_files: + result["data"]["parts"].append({ + "path": str(prt), + "name": prt.stem, + # Full introspection would be expensive, just list for now + }) + + # Find simulations + sim_files = list(model_dir.glob("*.sim")) if model_dir.exists() else [] + for sim in sim_files: + result["data"]["simulations"].append({ + "path": str(sim), + "name": sim.stem, + }) + + # Find OP2 results + op2_files = list(results_dir.rglob("*.op2")) if results_dir.exists() else [] + for op2 in op2_files[:10]: # Limit to 10 to avoid overwhelming + result["data"]["results"].append({ + "path": str(op2), + "name": op2.stem, + }) + + # Load config if exists + config_path = setup_dir / "optimization_config.json" + if config_path.exists(): + try: + with open(config_path, 'r') as f: + result["data"]["config"] = json.load(f) + except: + pass + + # Summary + result["data"]["summary"] = { + "part_count": len(result["data"]["parts"]), + "simulation_count": len(result["data"]["simulations"]), + "results_count": len(result["data"]["results"]), + "has_config": result["data"]["config"] is not None, + } + + return result + + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + path = sys.argv[1] + result = introspect_model(path) + print(json.dumps(result, indent=2)) + else: + print("Usage: python model_introspection.py ") + print("Supported: .prt, .sim, .op2 files") diff --git a/optimization_engine/hooks/nx_cad/part_manager.py b/optimization_engine/hooks/nx_cad/part_manager.py new file mode 100644 index 00000000..770e9064 --- /dev/null +++ b/optimization_engine/hooks/nx_cad/part_manager.py @@ -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 [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) diff --git a/optimization_engine/hooks/nx_cae/__init__.py b/optimization_engine/hooks/nx_cae/__init__.py new file mode 100644 index 00000000..e2d2eb9c --- /dev/null +++ b/optimization_engine/hooks/nx_cae/__init__.py @@ -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'] diff --git a/optimization_engine/hooks/nx_cae/solver_manager.py b/optimization_engine/hooks/nx_cae/solver_manager.py new file mode 100644 index 00000000..4e0050d2 --- /dev/null +++ b/optimization_engine/hooks/nx_cae/solver_manager.py @@ -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 [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 [solution_name]") diff --git a/optimization_engine/hooks/test_hooks.py b/optimization_engine/hooks/test_hooks.py new file mode 100644 index 00000000..756c925d --- /dev/null +++ b/optimization_engine/hooks/test_hooks.py @@ -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() diff --git a/optimization_engine/hooks/test_introspection.py b/optimization_engine/hooks/test_introspection.py new file mode 100644 index 00000000..be4d6edb --- /dev/null +++ b/optimization_engine/hooks/test_introspection.py @@ -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) diff --git a/optimization_engine/solve_simulation.py b/optimization_engine/solve_simulation.py index db5fac2f..58c931fd 100644 --- a/optimization_engine/solve_simulation.py +++ b/optimization_engine/solve_simulation.py @@ -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): """ - 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}") @@ -688,6 +694,192 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u partLoadStatus1.Dispose() 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.Post.UpdateUserGroupsFromSimPart(workSimPart) @@ -710,7 +902,7 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u psolutions1, NXOpen.CAE.SimSolution.SolveOption.Solve, 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) @@ -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") - # Save + # Save all try: - partSaveStatus = workSimPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, - NXOpen.BasePart.CloseAfterSave.FalseValue - ) + anyPartsModified, partSaveStatus = theSession.Parts.SaveAll() partSaveStatus.Dispose() - print(f"[JOURNAL] Saved!") + print(f"[JOURNAL] Saved all parts!") except: pass diff --git a/optimization_engine/study_wizard.py b/optimization_engine/study_wizard.py new file mode 100644 index 00000000..256e45d4 --- /dev/null +++ b/optimization_engine/study_wizard.py @@ -0,0 +1,1745 @@ +""" +Study Creation Wizard - Comprehensive study setup for Atomizer +=============================================================== + +A powerful, LLM-friendly wizard that automates the complete study creation workflow: +1. Model Introspection - Discover expressions, solutions, and available results +2. Configuration Generation - Build optimization_config.json from user requirements +3. Script Generation - Generate run_optimization.py with proper extractors +4. Documentation Generation - Create README.md, STUDY_REPORT.md, MODEL_INTROSPECTION.md + +This module is designed to work seamlessly with Claude Code skills. + +Usage: + from optimization_engine.study_wizard import StudyWizard + + wizard = StudyWizard( + study_name="my_optimization", + description="Optimize bracket for stiffness and mass" + ) + + # Step 1: Set model files + wizard.set_model_files( + prt_file="path/to/model.prt", + sim_file="path/to/model_sim1.sim" + ) + + # Step 2: Introspect model + introspection = wizard.introspect() + + # Step 3: Add design variables, objectives, constraints + wizard.add_design_variable("thickness", bounds=[5, 20], units="mm") + wizard.add_objective("mass", goal="minimize", extractor="extract_mass_from_bdf") + wizard.add_constraint("max_stress", type="less_than", threshold=250, units="MPa") + + # Step 4: Generate study + wizard.generate() + +Author: Atomizer Development Team +Version: 1.0.0 +Last Updated: 2025-12-06 +""" + +from pathlib import Path +from typing import Dict, Any, List, Optional, Tuple, Union +from dataclasses import dataclass, field +from datetime import datetime +import json +import logging +import shutil + +logger = logging.getLogger(__name__) + + +# ============================================================================= +# Data Classes +# ============================================================================= + +@dataclass +class DesignVariable: + """Design variable specification.""" + parameter: str + bounds: Tuple[float, float] + description: str = "" + units: str = "" + + def to_dict(self) -> Dict[str, Any]: + return { + "parameter": self.parameter, + "bounds": list(self.bounds), + "description": self.description or f"{self.parameter} design variable", + "units": self.units + } + + +@dataclass +class Objective: + """Optimization objective specification.""" + name: str + goal: str # "minimize" or "maximize" + extractor: str + params: Dict[str, Any] = field(default_factory=dict) + weight: float = 1.0 + description: str = "" + + def to_dict(self) -> Dict[str, Any]: + return { + "name": self.name, + "goal": self.goal, + "weight": self.weight, + "description": self.description or f"{self.goal.capitalize()} {self.name}", + "extraction": { + "action": self.extractor, + "domain": "result_extraction", + "params": self.params + } + } + + +@dataclass +class Constraint: + """Optimization constraint specification.""" + name: str + type: str # "less_than" or "greater_than" + threshold: float + extractor: str + params: Dict[str, Any] = field(default_factory=dict) + description: str = "" + units: str = "" + + def to_dict(self) -> Dict[str, Any]: + return { + "name": self.name, + "type": self.type, + "threshold": self.threshold, + "description": self.description or f"{self.name} constraint", + "extraction": { + "action": self.extractor, + "domain": "result_extraction", + "params": self.params + } + } + + +@dataclass +class IntrospectionResult: + """Results from model introspection.""" + success: bool + expressions: List[Dict[str, Any]] = field(default_factory=list) + solutions: List[Dict[str, Any]] = field(default_factory=list) + bodies: List[Dict[str, Any]] = field(default_factory=list) + mass_properties: Dict[str, Any] = field(default_factory=dict) + mesh_info: Dict[str, Any] = field(default_factory=dict) + available_results: Dict[str, bool] = field(default_factory=dict) + boundary_conditions: List[Dict[str, Any]] = field(default_factory=list) + loads: List[Dict[str, Any]] = field(default_factory=list) + materials: List[Dict[str, Any]] = field(default_factory=list) + subcases: List[int] = field(default_factory=list) + error: str = "" + + def get_expression_names(self) -> List[str]: + """Get list of expression names.""" + return [e.get('name', '') for e in self.expressions] + + def get_solution_names(self) -> List[str]: + """Get list of solution names.""" + return [s.get('name', '') for s in self.solutions] + + def suggest_design_variables(self) -> List[Dict[str, Any]]: + """Suggest potential design variables from expressions.""" + suggestions = [] + for expr in self.expressions: + name = expr.get('name', '') + value = expr.get('value') + + # Skip system/reference expressions + if name.startswith('p') and name[1:].isdigit(): + continue + if 'mass' in name.lower() and 'input' not in name.lower(): + continue + + if value is not None and isinstance(value, (int, float)): + # Suggest bounds based on value + if value > 0: + bounds = (value * 0.5, value * 1.5) + else: + bounds = (value * 1.5, value * 0.5) + + suggestions.append({ + 'name': name, + 'current_value': value, + 'suggested_bounds': bounds, + 'units': expr.get('unit', ''), + 'confidence': 'high' if any(kw in name.lower() for kw in ['thickness', 'angle', 'radius', 'length', 'width', 'height']) else 'medium' + }) + + return suggestions + + +# ============================================================================= +# Extractor Catalog +# ============================================================================= + +EXTRACTOR_CATALOG = { + # Mass extractors + "extract_mass_from_bdf": { + "module": "optimization_engine.extractors.bdf_mass_extractor", + "function": "extract_mass_from_bdf", + "input": ".dat/.bdf", + "output_unit": "kg", + "description": "Extract total mass from BDF/DAT file" + }, + "extract_part_mass": { + "module": "optimization_engine.extractors.extract_part_mass_material", + "function": "extract_part_mass", + "input": ".prt", + "output_unit": "kg", + "description": "Extract mass from NX part file via journal" + }, + + # Displacement extractors + "extract_displacement": { + "module": "optimization_engine.extractors.extract_displacement", + "function": "extract_displacement", + "input": ".op2", + "output_unit": "mm", + "description": "Extract displacement results from OP2" + }, + + # Stress extractors + "extract_solid_stress": { + "module": "optimization_engine.extractors.extract_von_mises_stress", + "function": "extract_solid_stress", + "input": ".op2", + "output_unit": "MPa", + "description": "Extract von Mises stress from OP2" + }, + "extract_principal_stress": { + "module": "optimization_engine.extractors.extract_principal_stress", + "function": "extract_principal_stress", + "input": ".op2", + "output_unit": "MPa", + "description": "Extract principal stresses (σ1, σ2, σ3)" + }, + + # Strain energy extractors + "extract_strain_energy": { + "module": "optimization_engine.extractors.extract_strain_energy", + "function": "extract_strain_energy", + "input": ".op2", + "output_unit": "J", + "description": "Extract strain energy from OP2" + }, + "extract_total_strain_energy": { + "module": "optimization_engine.extractors.extract_strain_energy", + "function": "extract_total_strain_energy", + "input": ".op2", + "output_unit": "J", + "description": "Extract total strain energy (convenience)" + }, + + # Reaction forces + "extract_spc_forces": { + "module": "optimization_engine.extractors.extract_spc_forces", + "function": "extract_spc_forces", + "input": ".op2", + "output_unit": "N", + "description": "Extract SPC/reaction forces from OP2" + }, + + # Frequency extractors + "extract_frequency": { + "module": "optimization_engine.extractors.extract_frequency", + "function": "extract_frequency", + "input": ".op2", + "output_unit": "Hz", + "description": "Extract natural frequencies from modal analysis" + }, + "get_first_frequency": { + "module": "optimization_engine.extractors.extract_modal_mass", + "function": "get_first_frequency", + "input": ".f06", + "output_unit": "Hz", + "description": "Get first natural frequency from F06" + }, + + # Temperature extractors (Phase 3) + "extract_temperature": { + "module": "optimization_engine.extractors.extract_temperature", + "function": "extract_temperature", + "input": ".op2", + "output_unit": "K/°C", + "description": "Extract temperatures from thermal analysis" + }, + "get_max_temperature": { + "module": "optimization_engine.extractors.extract_temperature", + "function": "get_max_temperature", + "input": ".op2", + "output_unit": "K/°C", + "description": "Get maximum temperature (convenience)" + }, + + # Modal mass (Phase 3) + "extract_modal_mass": { + "module": "optimization_engine.extractors.extract_modal_mass", + "function": "extract_modal_mass", + "input": ".f06", + "output_unit": "kg", + "description": "Extract modal effective mass from F06" + }, + + # Zernike (optical) + "extract_zernike_from_op2": { + "module": "optimization_engine.extractors.extract_zernike", + "function": "extract_zernike_from_op2", + "input": ".op2 + .bdf", + "output_unit": "nm", + "description": "Extract Zernike coefficients for optical surfaces" + } +} + + +# ============================================================================= +# Protocol Catalog +# ============================================================================= + +PROTOCOL_CATALOG = { + "protocol_10_single": { + "name": "Single-Objective IMSO", + "sampler": "TPESampler", + "description": "Adaptive single-objective optimization", + "use_when": ["single objective", "maximize or minimize one thing"], + "directions": 1 + }, + "protocol_11_multi": { + "name": "Multi-Objective NSGA-II", + "sampler": "NSGAIISampler", + "description": "Pareto-optimal multi-objective optimization", + "use_when": ["multiple objectives", "pareto front", "trade-offs"], + "directions": "multiple" + } +} + + +# ============================================================================= +# Study Wizard +# ============================================================================= + +class StudyWizard: + """ + Comprehensive study creation wizard for Atomizer. + + This wizard guides the complete study setup process: + 1. Model introspection (discover expressions, solutions, results) + 2. Configuration generation (optimization_config.json) + 3. Script generation (run_optimization.py, reset_study.py) + 4. Documentation generation (README.md, STUDY_REPORT.md, MODEL_INTROSPECTION.md) + """ + + def __init__( + self, + study_name: str, + description: str = "", + studies_dir: Optional[Path] = None + ): + """ + Initialize study wizard. + + Args: + study_name: Name of the study (used for directory name) + description: Human-readable description + studies_dir: Base directory for studies (default: project/studies/) + """ + self.study_name = study_name + self.description = description + + # Set studies directory + if studies_dir is None: + # Find project root by looking for CLAUDE.md + current = Path(__file__).parent + while current != current.parent: + if (current / "CLAUDE.md").exists(): + studies_dir = current / "studies" + break + current = current.parent + else: + studies_dir = Path.cwd() / "studies" + + self.studies_dir = Path(studies_dir) + self.study_dir = self.studies_dir / study_name + + # Model files + self.prt_file: Optional[Path] = None + self.sim_file: Optional[Path] = None + self.fem_file: Optional[Path] = None + self.op2_file: Optional[Path] = None + + # Configuration + self.design_variables: List[DesignVariable] = [] + self.objectives: List[Objective] = [] + self.constraints: List[Constraint] = [] + + # Introspection results + self.introspection: Optional[IntrospectionResult] = None + + # Settings + self.protocol = "protocol_11_multi" # Default to multi-objective + self.n_trials = 100 + self.timeout_per_trial = 400 + self.neural_enabled = False + + logger.info(f"StudyWizard initialized for '{study_name}'") + logger.info(f" Study directory: {self.study_dir}") + + # ========================================================================= + # Model File Management + # ========================================================================= + + def set_model_files( + self, + prt_file: Union[str, Path], + sim_file: Optional[Union[str, Path]] = None, + fem_file: Optional[Union[str, Path]] = None, + op2_file: Optional[Union[str, Path]] = None + ) -> "StudyWizard": + """ + Set model files for the study. + + Args: + prt_file: Path to NX part file (.prt) + sim_file: Path to simulation file (.sim) - auto-detected if None + fem_file: Path to FEM file (.fem) - auto-detected if None + op2_file: Path to OP2 results file (.op2) - optional + + Returns: + Self for method chaining + """ + self.prt_file = Path(prt_file) + + if not self.prt_file.exists(): + raise FileNotFoundError(f"Part file not found: {self.prt_file}") + + # Auto-detect sim file + if sim_file: + self.sim_file = Path(sim_file) + else: + # Look for *_sim1.sim or *.sim in same directory + prt_dir = self.prt_file.parent + sim_candidates = list(prt_dir.glob("*_sim1.sim")) + list(prt_dir.glob("*.sim")) + if sim_candidates: + self.sim_file = sim_candidates[0] + logger.info(f" Auto-detected sim file: {self.sim_file.name}") + + # Auto-detect fem file + if fem_file: + self.fem_file = Path(fem_file) + elif self.sim_file: + fem_candidates = list(self.sim_file.parent.glob("*_fem1.fem")) + list(self.sim_file.parent.glob("*.fem")) + if fem_candidates: + self.fem_file = fem_candidates[0] + logger.info(f" Auto-detected fem file: {self.fem_file.name}") + + # Set op2 file if provided + if op2_file: + self.op2_file = Path(op2_file) + + logger.info(f"Model files set:") + logger.info(f" PRT: {self.prt_file}") + logger.info(f" SIM: {self.sim_file}") + logger.info(f" FEM: {self.fem_file}") + + return self + + # ========================================================================= + # Model Introspection + # ========================================================================= + + def introspect(self, run_baseline: bool = False) -> IntrospectionResult: + """ + Introspect model to discover expressions, solutions, and available results. + + Uses the model_introspection module for comprehensive analysis. + + Args: + run_baseline: If True, run a baseline solve to generate OP2 for introspection + + Returns: + IntrospectionResult with all discovered information + """ + logger.info("=" * 60) + logger.info("MODEL INTROSPECTION") + logger.info("=" * 60) + + try: + from optimization_engine.hooks.nx_cad.model_introspection import ( + introspect_part, + introspect_simulation, + introspect_op2 + ) + except ImportError: + logger.warning("Model introspection module not available") + self.introspection = IntrospectionResult( + success=False, + error="Model introspection module not available" + ) + return self.introspection + + result = IntrospectionResult(success=True) + + # Introspect part file + if self.prt_file and self.prt_file.exists(): + logger.info(f"\nIntrospecting part: {self.prt_file.name}") + part_result = introspect_part(str(self.prt_file)) + + if part_result.get('success'): + data = part_result.get('data', {}) + result.expressions = data.get('expressions', []) + result.bodies = data.get('bodies', []) + result.mass_properties = data.get('mass_properties', {}) + + logger.info(f" Found {len(result.expressions)} expressions") + logger.info(f" Found {len(result.bodies)} bodies") + if result.mass_properties: + logger.info(f" Mass: {result.mass_properties.get('mass', 'N/A')} kg") + else: + logger.warning(f" Part introspection failed: {part_result.get('error')}") + + # Introspect simulation file + if self.sim_file and self.sim_file.exists(): + logger.info(f"\nIntrospecting simulation: {self.sim_file.name}") + sim_result = introspect_simulation(str(self.sim_file)) + + if sim_result.get('success'): + data = sim_result.get('data', {}) + result.solutions = data.get('solutions', []) + result.boundary_conditions = data.get('boundary_conditions', []) + result.loads = data.get('loads', []) + result.materials = data.get('materials', []) + result.mesh_info = data.get('mesh_info', {}) + + logger.info(f" Found {len(result.solutions)} solutions") + logger.info(f" Found {len(result.boundary_conditions)} boundary conditions") + logger.info(f" Found {len(result.loads)} loads") + else: + logger.warning(f" Simulation introspection failed: {sim_result.get('error')}") + + # Introspect OP2 file if available + if self.op2_file and self.op2_file.exists(): + logger.info(f"\nIntrospecting OP2: {self.op2_file.name}") + op2_result = introspect_op2(str(self.op2_file)) + + if op2_result.get('success'): + data = op2_result.get('data', {}) + result.available_results = data.get('available_results', {}) + result.subcases = data.get('subcases', []) + + logger.info(f" Available results: {result.available_results}") + logger.info(f" Subcases: {result.subcases}") + else: + logger.warning(f" OP2 introspection failed: {op2_result.get('error')}") + + self.introspection = result + return result + + # ========================================================================= + # Design Variable, Objective, Constraint Management + # ========================================================================= + + def add_design_variable( + self, + parameter: str, + bounds: Tuple[float, float], + description: str = "", + units: str = "" + ) -> "StudyWizard": + """ + Add a design variable to the study. + + Args: + parameter: Name of the NX expression to vary + bounds: (min, max) bounds for the variable + description: Human-readable description + units: Units (e.g., "mm", "degrees") + + Returns: + Self for method chaining + """ + dv = DesignVariable( + parameter=parameter, + bounds=bounds, + description=description, + units=units + ) + self.design_variables.append(dv) + logger.info(f"Added design variable: {parameter} [{bounds[0]}, {bounds[1]}] {units}") + return self + + def add_objective( + self, + name: str, + goal: str, + extractor: str, + params: Optional[Dict[str, Any]] = None, + weight: float = 1.0, + description: str = "" + ) -> "StudyWizard": + """ + Add an optimization objective. + + Args: + name: Objective name (e.g., "mass", "stiffness") + goal: "minimize" or "maximize" + extractor: Extractor function name from catalog + params: Additional parameters for extractor + weight: Weight for multi-objective optimization + description: Human-readable description + + Returns: + Self for method chaining + """ + if extractor not in EXTRACTOR_CATALOG: + logger.warning(f"Extractor '{extractor}' not in catalog, proceeding anyway") + + obj = Objective( + name=name, + goal=goal, + extractor=extractor, + params=params or {}, + weight=weight, + description=description + ) + self.objectives.append(obj) + logger.info(f"Added objective: {goal} {name} (extractor: {extractor})") + return self + + def add_constraint( + self, + name: str, + constraint_type: str, + threshold: float, + extractor: str, + params: Optional[Dict[str, Any]] = None, + description: str = "", + units: str = "" + ) -> "StudyWizard": + """ + Add an optimization constraint. + + Args: + name: Constraint name (e.g., "max_stress") + constraint_type: "less_than" or "greater_than" + threshold: Constraint threshold value + extractor: Extractor function name from catalog + params: Additional parameters for extractor + description: Human-readable description + units: Units for display + + Returns: + Self for method chaining + """ + const = Constraint( + name=name, + type=constraint_type, + threshold=threshold, + extractor=extractor, + params=params or {}, + description=description, + units=units + ) + self.constraints.append(const) + logger.info(f"Added constraint: {name} {constraint_type} {threshold} {units}") + return self + + # ========================================================================= + # Settings + # ========================================================================= + + def set_protocol(self, protocol: str) -> "StudyWizard": + """Set optimization protocol.""" + if protocol not in PROTOCOL_CATALOG: + raise ValueError(f"Unknown protocol: {protocol}. Available: {list(PROTOCOL_CATALOG.keys())}") + self.protocol = protocol + return self + + def set_trials(self, n_trials: int) -> "StudyWizard": + """Set number of optimization trials.""" + self.n_trials = n_trials + return self + + def enable_neural(self, enabled: bool = True) -> "StudyWizard": + """Enable/disable neural acceleration.""" + self.neural_enabled = enabled + return self + + # ========================================================================= + # Generation + # ========================================================================= + + def generate(self, copy_model_files: bool = True) -> Dict[str, Path]: + """ + Generate complete study structure. + + Creates: + - Study directory structure + - optimization_config.json + - workflow_config.json + - run_optimization.py + - reset_study.py + - README.md + - STUDY_REPORT.md + - MODEL_INTROSPECTION.md + + Args: + copy_model_files: If True, copy model files to study directory + + Returns: + Dict of generated file paths + """ + logger.info("=" * 60) + logger.info("GENERATING STUDY") + logger.info("=" * 60) + + # Validate + if not self.design_variables: + raise ValueError("At least one design variable is required") + if not self.objectives: + raise ValueError("At least one objective is required") + + # Create directory structure + setup_dir = self.study_dir / "1_setup" + model_dir = setup_dir / "model" + results_dir = self.study_dir / "2_results" + + setup_dir.mkdir(parents=True, exist_ok=True) + model_dir.mkdir(exist_ok=True) + results_dir.mkdir(exist_ok=True) + + logger.info(f"Created study directory: {self.study_dir}") + + generated_files = {} + + # Copy model files + if copy_model_files and self.prt_file: + self._copy_model_files(model_dir) + + # Generate optimization_config.json + config_path = setup_dir / "optimization_config.json" + self._generate_config(config_path) + generated_files['optimization_config'] = config_path + + # Generate workflow_config.json + workflow_path = setup_dir / "workflow_config.json" + self._generate_workflow_config(workflow_path) + generated_files['workflow_config'] = workflow_path + + # Generate run_optimization.py + run_script_path = self.study_dir / "run_optimization.py" + self._generate_run_script(run_script_path) + generated_files['run_optimization'] = run_script_path + + # Generate reset_study.py + reset_script_path = self.study_dir / "reset_study.py" + self._generate_reset_script(reset_script_path) + generated_files['reset_study'] = reset_script_path + + # Generate documentation + readme_path = self.study_dir / "README.md" + self._generate_readme(readme_path) + generated_files['readme'] = readme_path + + report_path = self.study_dir / "STUDY_REPORT.md" + self._generate_study_report(report_path) + generated_files['study_report'] = report_path + + introspection_path = self.study_dir / "MODEL_INTROSPECTION.md" + self._generate_introspection_report(introspection_path) + generated_files['model_introspection'] = introspection_path + + logger.info("=" * 60) + logger.info("STUDY GENERATION COMPLETE") + logger.info("=" * 60) + logger.info(f"\nGenerated files:") + for name, path in generated_files.items(): + logger.info(f" {name}: {path}") + + logger.info(f"\nNext steps:") + logger.info(f" 1. Review generated files") + logger.info(f" 2. cd {self.study_dir}") + logger.info(f" 3. python run_optimization.py --discover") + logger.info(f" 4. python run_optimization.py --validate") + logger.info(f" 5. python run_optimization.py --run --trials {self.n_trials}") + + return generated_files + + def _copy_model_files(self, model_dir: Path): + """Copy model files to study directory.""" + logger.info("Copying model files...") + + files_to_copy = [self.prt_file, self.sim_file, self.fem_file] + for src in files_to_copy: + if src and src.exists(): + dst = model_dir / src.name + if not dst.exists(): + shutil.copy2(src, dst) + logger.info(f" Copied: {src.name}") + else: + logger.info(f" Already exists: {src.name}") + + def _generate_config(self, path: Path): + """Generate optimization_config.json.""" + logger.info(f"Generating: {path.name}") + + # Determine simulation files + model_name = self.prt_file.stem if self.prt_file else "model" + sim_name = self.sim_file.stem if self.sim_file else f"{model_name}_sim1" + + # Infer dat and op2 file names + dat_file = f"{sim_name.lower()}-solution_1.dat" + op2_file = f"{sim_name.lower()}-solution_1.op2" + + # Determine sampler from protocol + sampler = PROTOCOL_CATALOG.get(self.protocol, {}).get('sampler', 'NSGAIISampler') + + config = { + "study_name": self.study_name, + "description": self.description, + "engineering_context": f"Generated by StudyWizard on {datetime.now().strftime('%Y-%m-%d %H:%M')}", + + "template_info": { + "category": "structural", + "analysis_type": "static", + "typical_applications": [], + "neural_enabled": self.neural_enabled + }, + + "optimization_settings": { + "protocol": self.protocol, + "n_trials": self.n_trials, + "sampler": sampler, + "pruner": None, + "timeout_per_trial": self.timeout_per_trial + }, + + "design_variables": [dv.to_dict() for dv in self.design_variables], + "objectives": [obj.to_dict() for obj in self.objectives], + "constraints": [const.to_dict() for const in self.constraints], + + "simulation": { + "model_file": self.prt_file.name if self.prt_file else "", + "sim_file": self.sim_file.name if self.sim_file else "", + "fem_file": self.fem_file.name if self.fem_file else "", + "solver": "nastran", + "analysis_types": ["static"], + "solution_name": "Solution 1", + "dat_file": dat_file, + "op2_file": op2_file + }, + + "result_extraction": self._build_extraction_config(), + + "reporting": { + "generate_plots": True, + "save_incremental": True, + "llm_summary": True, + "generate_pareto_front": len(self.objectives) > 1 + }, + + "neural_acceleration": { + "enabled": self.neural_enabled, + "min_training_points": 50, + "auto_train": True, + "epochs": 100, + "validation_split": 0.2 + } + } + + with open(path, 'w') as f: + json.dump(config, f, indent=2) + + def _build_extraction_config(self) -> Dict[str, Any]: + """Build result_extraction section of config.""" + extraction = {} + + # Add extractors for objectives + for obj in self.objectives: + extractor_info = EXTRACTOR_CATALOG.get(obj.extractor, {}) + extraction[obj.name] = { + "method": obj.extractor, + "extractor_module": extractor_info.get('module', ''), + "function": extractor_info.get('function', obj.extractor), + "output_unit": extractor_info.get('output_unit', '') + } + + # Add extractors for constraints + for const in self.constraints: + if const.name not in extraction: + extractor_info = EXTRACTOR_CATALOG.get(const.extractor, {}) + extraction[const.name] = { + "method": const.extractor, + "extractor_module": extractor_info.get('module', ''), + "function": extractor_info.get('function', const.extractor), + "output_unit": extractor_info.get('output_unit', const.units) + } + + return extraction + + def _generate_workflow_config(self, path: Path): + """Generate workflow_config.json.""" + logger.info(f"Generating: {path.name}") + + config = { + "workflow_id": f"{self.study_name}_workflow", + "description": f"Workflow for {self.study_name}", + "steps": [] + } + + with open(path, 'w') as f: + json.dump(config, f, indent=2) + + def _generate_run_script(self, path: Path): + """Generate run_optimization.py script.""" + logger.info(f"Generating: {path.name}") + + # Build import statements for extractors + extractor_imports = set() + for obj in self.objectives: + info = EXTRACTOR_CATALOG.get(obj.extractor, {}) + if info.get('module'): + extractor_imports.add(f"from {info['module']} import {info.get('function', obj.extractor)}") + + for const in self.constraints: + info = EXTRACTOR_CATALOG.get(const.extractor, {}) + if info.get('module'): + extractor_imports.add(f"from {info['module']} import {info.get('function', const.extractor)}") + + # Determine if multi-objective + is_multi = len(self.objectives) > 1 + sampler = PROTOCOL_CATALOG.get(self.protocol, {}).get('sampler', 'NSGAIISampler') + + # Build objective function extraction code + extraction_code = self._build_extraction_code() + + # Build return statement + if is_multi: + returns = ", ".join([f"obj_{obj.name}" for obj in self.objectives]) + return_stmt = f"return ({returns})" + else: + obj = self.objectives[0] + return_stmt = f"return obj_{obj.name}" + + script = f'''""" +{self.study_name} - Optimization Script +{"=" * 60} + +{self.description} + +Protocol: {PROTOCOL_CATALOG.get(self.protocol, {}).get('name', self.protocol)} + +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 {self.n_trials} + +Generated by StudyWizard on {datetime.now().strftime('%Y-%m-%d %H:%M')} +""" + +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 {sampler} + +# Core imports +from optimization_engine.nx_solver import NXSolver +from optimization_engine.logger import get_logger + +# Extractor imports +{chr(10).join(sorted(extractor_imports))} + + +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[" + ", ".join(["float"] * len(self.objectives)) + "]" if is_multi else "float"}: + """ + Objective function for optimization. + + {"Returns tuple of objectives for multi-objective optimization." if is_multi else "Returns single objective value."} + """ + # 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 {"(" + ", ".join(["float('inf')"] * len(self.objectives)) + ")" if is_multi else "float('inf')"} + + op2_file = result['op2_file'] + dat_file = model_dir / config['simulation']['dat_file'] + +{extraction_code} + + # Check constraints + feasible = True + constraint_results = {{}} +{self._build_constraint_check_code()} + + # Set user attributes +{self._build_user_attrs_code()} + trial.set_user_attr('feasible', feasible) + + objectives = {{{", ".join([f"'{obj.name}': obj_{obj.name}" for obj in self.objectives])}}} + logger.trial_complete(trial.number, objectives, constraint_results, feasible) + + {return_stmt} + + except Exception as e: + logger.trial_failed(trial.number, str(e)) + return {"(" + ", ".join(["float('inf')"] * len(self.objectives)) + ")" if is_multi else "float('inf')"} + + +def main(): + """Main optimization workflow.""" + parser = argparse.ArgumentParser(description='{self.study_name}') + + 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={self.n_trials}, 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 = "{self.study_name}" + + # Initialize + logger = get_logger(study_name, study_dir=results_dir) + config = load_config(config_path) + nx_solver = NXSolver() + + 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={sampler}({"population_size=5, seed=42" if is_multi else "seed=42"}), + {"directions=['minimize'] * " + str(len(self.objectives)) if is_multi else "direction='minimize'"}, + 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={sampler}({"population_size=20, seed=42" if is_multi else "seed=42"}) + ) + else: + study = optuna.create_study( + study_name=study_name, + storage=storage, + sampler={sampler}({"population_size=20, seed=42" if is_multi else "seed=42"}), + {"directions=['minimize'] * " + str(len(self.objectives)) if is_multi else "direction='minimize'"}, + load_if_exists=True + ) + + logger.study_start(study_name, args.trials, "{sampler}") + + 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" if is_multi else "best_trial = study.best_trial"} + 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()) +''' + + with open(path, 'w') as f: + f.write(script) + + def _build_extraction_code(self) -> str: + """Build extraction code for objective function.""" + lines = [] + lines.append(" # Extract results") + + for obj in self.objectives: + info = EXTRACTOR_CATALOG.get(obj.extractor, {}) + func = info.get('function', obj.extractor) + + # Determine source file + if '.dat' in info.get('input', '') or '.bdf' in info.get('input', ''): + source = "dat_file" + else: + source = "op2_file" + + # Build extraction call + if 'displacement' in obj.extractor.lower(): + lines.append(f" disp_result = {func}({source}, subcase=1)") + lines.append(f" max_displacement = disp_result['max_displacement']") + if obj.goal == 'maximize': + lines.append(f" # For stiffness maximization, use inverse of displacement") + lines.append(f" applied_force = 1000.0 # N - adjust based on your model") + lines.append(f" obj_{obj.name} = -applied_force / max(abs(max_displacement), 1e-6)") + else: + lines.append(f" obj_{obj.name} = max_displacement") + elif 'mass' in obj.extractor.lower(): + lines.append(f" obj_{obj.name} = {func}(str({source}))") + if obj.goal == 'maximize': + lines.append(f" obj_{obj.name} = -obj_{obj.name} # Negate for maximization") + elif 'stress' in obj.extractor.lower(): + lines.append(f" stress_result = {func}({source}, subcase=1)") + lines.append(f" obj_{obj.name} = stress_result.get('max_von_mises', float('inf'))") + if obj.goal == 'maximize': + lines.append(f" obj_{obj.name} = -obj_{obj.name} # Negate for maximization") + else: + # Generic extraction + lines.append(f" obj_{obj.name} = {func}({source})") + if obj.goal == 'maximize': + lines.append(f" obj_{obj.name} = -obj_{obj.name} # Negate for maximization") + + lines.append(f" logger.info(f' {obj.name}: {{obj_{obj.name}}}')") + lines.append("") + + return "\n".join(lines) + + def _build_constraint_check_code(self) -> str: + """Build constraint checking code.""" + if not self.constraints: + return " pass # No constraints defined" + + lines = [] + for const in self.constraints: + lines.append(f" # Check {const.name}") + + # Get the value to check (may need extraction) + if any(obj.name == const.name for obj in self.objectives): + # Already extracted as objective + value_var = f"obj_{const.name}" + else: + # Need to extract + info = EXTRACTOR_CATALOG.get(const.extractor, {}) + func = info.get('function', const.extractor) + source = "dat_file" if '.dat' in info.get('input', '') else "op2_file" + lines.append(f" const_{const.name} = {func}({source})") + value_var = f"const_{const.name}" + + lines.append(f" constraint_results['{const.name}'] = {value_var}") + + if const.type == "less_than": + lines.append(f" if {value_var} > {const.threshold}:") + else: + lines.append(f" if {value_var} < {const.threshold}:") + + lines.append(f" feasible = False") + lines.append(f" logger.warning(f' Constraint violation: {const.name} = {{{value_var}}} vs {const.threshold}')") + lines.append("") + + return "\n".join(lines) + + def _build_user_attrs_code(self) -> str: + """Build user attributes setting code.""" + lines = [] + for obj in self.objectives: + lines.append(f" trial.set_user_attr('{obj.name}', obj_{obj.name})") + return "\n".join(lines) + + def _generate_reset_script(self, path: Path): + """Generate reset_study.py script.""" + logger.info(f"Generating: {path.name}") + + script = f'''""" +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() +''' + + with open(path, 'w') as f: + f.write(script) + + def _generate_readme(self, path: Path): + """Generate README.md documentation.""" + logger.info(f"Generating: {path.name}") + + # Build design variables table + dv_table = "| Parameter | Bounds | Units | Description |\n|-----------|--------|-------|-------------|\n" + for dv in self.design_variables: + dv_table += f"| `{dv.parameter}` | [{dv.bounds[0]}, {dv.bounds[1]}] | {dv.units} | {dv.description} |\n" + + # Build objectives table + obj_table = "| Objective | Goal | Extractor | Weight |\n|-----------|------|-----------|--------|\n" + for obj in self.objectives: + obj_table += f"| {obj.name} | {obj.goal} | `{obj.extractor}` | {obj.weight} |\n" + + # Build constraints table + const_table = "| Constraint | Type | Threshold | Units |\n|------------|------|-----------|-------|\n" + for const in self.constraints: + const_table += f"| {const.name} | {const.type} | {const.threshold} | {const.units} |\n" + + protocol_info = PROTOCOL_CATALOG.get(self.protocol, {}) + + readme = f'''# {self.study_name} + +{self.description} + +**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M')} +**Protocol**: {protocol_info.get('name', self.protocol)} +**Trials**: {self.n_trials} + +--- + +## 1. Engineering Problem + +{self.description} + +--- + +## 2. Mathematical Formulation + +### Design Variables + +{dv_table} + +### Objectives + +{obj_table} + +### Constraints + +{const_table if self.constraints else "No constraints defined."} + +--- + +## 3. Optimization Algorithm + +- **Protocol**: {self.protocol} +- **Sampler**: {protocol_info.get('sampler', 'TPESampler')} +- **Trials**: {self.n_trials} +- **Neural Acceleration**: {"Enabled" if self.neural_enabled else "Disabled"} + +--- + +## 4. Simulation Pipeline + +``` +Design Variables → NX Expression Update → Nastran Solve → Result Extraction → Objective Evaluation +``` + +--- + +## 5. Result Extraction Methods + +| Result | Extractor | Source | +|--------|-----------|--------| +{chr(10).join([f"| {obj.name} | `{obj.extractor}` | OP2/DAT |" for obj in self.objectives])} + +--- + +## 6. Study File Structure + +``` +{self.study_name}/ +├── 1_setup/ +│ ├── model/ +│ │ ├── {self.prt_file.name if self.prt_file else "Model.prt"} +│ │ ├── {self.sim_file.name if self.sim_file else "Model_sim1.sim"} +│ │ └── {self.fem_file.name if self.fem_file else "Model_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 {self.n_trials} + +# 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 {self.protocol}](../../docs/protocols/system/) +- [Extractor Library](../../docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md) +''' + + with open(path, 'w') as f: + f.write(readme) + + def _generate_study_report(self, path: Path): + """Generate STUDY_REPORT.md template.""" + logger.info(f"Generating: {path.name}") + + report = f'''# Study Report: {self.study_name} + +**Status**: Not Started +**Created**: {datetime.now().strftime('%Y-%m-%d %H:%M')} +**Last Updated**: {datetime.now().strftime('%Y-%m-%d %H:%M')} + +--- + +## 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 {self.n_trials}` +5. [ ] Analyze results and update this report + +--- + +*Generated by StudyWizard* +''' + + with open(path, 'w') as f: + f.write(report) + + def _generate_introspection_report(self, path: Path): + """Generate MODEL_INTROSPECTION.md report.""" + logger.info(f"Generating: {path.name}") + + # Build expressions table + if self.introspection and self.introspection.expressions: + expr_table = "| Name | Value | Unit | Optimization Candidate |\n|------|-------|------|------------------------|\n" + for expr in self.introspection.expressions[:20]: # Limit to 20 + name = expr.get('name', '') + value = expr.get('value', 'N/A') + unit = expr.get('unit', '') + candidate = "✓ High" if any(kw in name.lower() for kw in ['thickness', 'angle', 'radius', 'length']) else "Medium" + expr_table += f"| {name} | {value} | {unit} | {candidate} |\n" + else: + expr_table = "*Run introspection to discover expressions.*" + + # Build solutions table + if self.introspection and self.introspection.solutions: + sol_table = "| Solution | Type | Status |\n|----------|------|--------|\n" + for sol in self.introspection.solutions: + sol_table += f"| {sol.get('name', 'Unknown')} | {sol.get('type', 'Static')} | ✓ Active |\n" + else: + sol_table = "*Run introspection to discover solutions.*" + + report = f'''# Model Introspection Report + +**Study**: {self.study_name} +**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M')} +**Introspection Version**: 1.0 + +--- + +## 1. Files Discovered + +| Type | File | Status | +|------|------|--------| +| Part (.prt) | {self.prt_file.name if self.prt_file else "Not set"} | {"✓ Found" if self.prt_file and self.prt_file.exists() else "❌ Missing"} | +| Simulation (.sim) | {self.sim_file.name if self.sim_file else "Not set"} | {"✓ Found" if self.sim_file and self.sim_file.exists() else "❌ Missing"} | +| FEM (.fem) | {self.fem_file.name if self.fem_file else "Not set"} | {"✓ Found" if self.fem_file and self.fem_file.exists() else "⚠ Will be created"} | + +--- + +## 2. Expressions (Potential Design Variables) + +{expr_table} + +--- + +## 3. Solutions + +{sol_table} + +--- + +## 4. Available Results + +| Result Type | Available | Subcases | +|-------------|-----------|----------| +| Displacement | {"✓" if self.introspection and self.introspection.available_results.get('displacement') else "?"} | - | +| Stress | {"✓" if self.introspection and self.introspection.available_results.get('stress') else "?"} | - | +| SPC Forces | {"✓" if self.introspection and self.introspection.available_results.get('spc_forces') else "?"} | - | + +--- + +## 5. Optimization Configuration + +### Selected Design Variables + +{chr(10).join([f"- `{dv.parameter}`: [{dv.bounds[0]}, {dv.bounds[1]}] {dv.units}" for dv in self.design_variables]) if self.design_variables else "*No design variables configured yet.*"} + +### Selected Objectives + +{chr(10).join([f"- {obj.goal.capitalize()} `{obj.name}` using `{obj.extractor}`" for obj in self.objectives]) if self.objectives else "*No objectives configured yet.*"} + +### Selected Constraints + +{chr(10).join([f"- `{c.name}` {c.type} {c.threshold} {c.units}" for c in self.constraints]) if self.constraints else "*No constraints configured.*"} + +--- + +*Ready to create optimization study? Run `python run_optimization.py --discover` to proceed.* +''' + + with open(path, 'w') as f: + f.write(report) + + +# ============================================================================= +# Convenience Functions +# ============================================================================= + +def create_study( + study_name: str, + description: str, + prt_file: Union[str, Path], + design_variables: List[Dict[str, Any]], + objectives: List[Dict[str, Any]], + constraints: Optional[List[Dict[str, Any]]] = None, + n_trials: int = 100, + protocol: str = "protocol_11_multi" +) -> Dict[str, Path]: + """ + Convenience function to create a complete study in one call. + + Args: + study_name: Name of the study + description: Human-readable description + prt_file: Path to NX part file + design_variables: List of design variable dicts with keys: + - parameter: str + - bounds: [min, max] + - units: str (optional) + - description: str (optional) + objectives: List of objective dicts with keys: + - name: str + - goal: "minimize" or "maximize" + - extractor: str + - params: dict (optional) + constraints: List of constraint dicts with keys: + - name: str + - type: "less_than" or "greater_than" + - threshold: float + - extractor: str + - units: str (optional) + n_trials: Number of optimization trials + protocol: Optimization protocol + + Returns: + Dict of generated file paths + + Example: + create_study( + study_name="bracket_opt", + description="Optimize bracket for stiffness", + prt_file="Bracket.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"} + ] + ) + """ + wizard = StudyWizard(study_name, description) + wizard.set_model_files(prt_file) + wizard.set_protocol(protocol) + wizard.set_trials(n_trials) + + # Add design variables + for dv in design_variables: + wizard.add_design_variable( + parameter=dv['parameter'], + bounds=tuple(dv['bounds']), + units=dv.get('units', ''), + description=dv.get('description', '') + ) + + # Add objectives + for obj in objectives: + wizard.add_objective( + name=obj['name'], + goal=obj['goal'], + extractor=obj['extractor'], + params=obj.get('params', {}), + weight=obj.get('weight', 1.0), + description=obj.get('description', '') + ) + + # Add constraints + for const in (constraints or []): + wizard.add_constraint( + name=const['name'], + constraint_type=const['type'], + threshold=const['threshold'], + extractor=const['extractor'], + params=const.get('params', {}), + units=const.get('units', ''), + description=const.get('description', '') + ) + + # Run introspection if model files exist + try: + wizard.introspect() + except Exception as e: + logger.warning(f"Introspection failed: {e}") + + return wizard.generate() + + +def list_extractors() -> Dict[str, Dict[str, Any]]: + """Return the extractor catalog.""" + return EXTRACTOR_CATALOG.copy() + + +def list_protocols() -> Dict[str, Dict[str, Any]]: + """Return the protocol catalog.""" + return PROTOCOL_CATALOG.copy() + + +# ============================================================================= +# CLI +# ============================================================================= + +def main(): + """CLI for study wizard.""" + import argparse + + parser = argparse.ArgumentParser(description="Atomizer Study Creation Wizard") + parser.add_argument('--name', required=True, help='Study name') + parser.add_argument('--description', default='', help='Study description') + parser.add_argument('--prt', required=True, help='Path to PRT file') + parser.add_argument('--list-extractors', action='store_true', help='List available extractors') + + args = parser.parse_args() + + if args.list_extractors: + print("\nAvailable Extractors:") + print("=" * 60) + for name, info in EXTRACTOR_CATALOG.items(): + print(f"\n{name}:") + print(f" Input: {info.get('input', 'N/A')}") + print(f" Output: {info.get('output_unit', 'N/A')}") + print(f" {info.get('description', '')}") + return 0 + + # Interactive wizard would go here + print(f"\nStudy Wizard initialized for: {args.name}") + print("Use the Python API for full functionality.") + + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/studies/bracket_pareto_3obj/1_setup/model/Bracket.prt b/studies/bracket_pareto_3obj/1_setup/model/Bracket.prt new file mode 100644 index 00000000..f91d1979 Binary files /dev/null and b/studies/bracket_pareto_3obj/1_setup/model/Bracket.prt differ diff --git a/studies/bracket_pareto_3obj/1_setup/model/Bracket_fem1.fem b/studies/bracket_pareto_3obj/1_setup/model/Bracket_fem1.fem new file mode 100644 index 00000000..877b79a8 Binary files /dev/null and b/studies/bracket_pareto_3obj/1_setup/model/Bracket_fem1.fem differ diff --git a/studies/bracket_pareto_3obj/1_setup/model/Bracket_fem1_i.prt b/studies/bracket_pareto_3obj/1_setup/model/Bracket_fem1_i.prt new file mode 100644 index 00000000..55e89d1b Binary files /dev/null and b/studies/bracket_pareto_3obj/1_setup/model/Bracket_fem1_i.prt differ diff --git a/studies/bracket_pareto_3obj/1_setup/model/Bracket_sim1.sim b/studies/bracket_pareto_3obj/1_setup/model/Bracket_sim1.sim new file mode 100644 index 00000000..6de323e5 Binary files /dev/null and b/studies/bracket_pareto_3obj/1_setup/model/Bracket_sim1.sim differ diff --git a/studies/bracket_pareto_3obj/1_setup/optimization_config.json b/studies/bracket_pareto_3obj/1_setup/optimization_config.json new file mode 100644 index 00000000..6a3c1bc2 --- /dev/null +++ b/studies/bracket_pareto_3obj/1_setup/optimization_config.json @@ -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" + } +} \ No newline at end of file diff --git a/studies/bracket_pareto_3obj/1_setup/workflow_config.json b/studies/bracket_pareto_3obj/1_setup/workflow_config.json new file mode 100644 index 00000000..0dd0408a --- /dev/null +++ b/studies/bracket_pareto_3obj/1_setup/workflow_config.json @@ -0,0 +1,5 @@ +{ + "workflow_id": "bracket_pareto_3obj_workflow", + "description": "Workflow for bracket_pareto_3obj", + "steps": [] +} \ No newline at end of file diff --git a/studies/bracket_pareto_3obj/2_results/nn_optimization_state.json b/studies/bracket_pareto_3obj/2_results/nn_optimization_state.json new file mode 100644 index 00000000..b3663e14 --- /dev/null +++ b/studies/bracket_pareto_3obj/2_results/nn_optimization_state.json @@ -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 + ] + } + ] +} \ No newline at end of file diff --git a/studies/bracket_pareto_3obj/2_results/nn_pareto_front.json b/studies/bracket_pareto_3obj/2_results/nn_pareto_front.json new file mode 100644 index 00000000..8f34e961 --- /dev/null +++ b/studies/bracket_pareto_3obj/2_results/nn_pareto_front.json @@ -0,0 +1,7940 @@ +{ + "type": "nn_optimization", + "n_trials": 1000, + "time_seconds": 87.70169997215271, + "n_pareto": 661, + "trials": [ + { + "number": 0, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.15462589263916016, + 90.49411010742188, + -19956.513671875 + ] + }, + { + "number": 1, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 47.959754525911094 + }, + "values": [ + 0.1316341757774353, + 80.95538330078125, + -15403.2138671875 + ] + }, + { + "number": 2, + "params": { + "support_angle": 27.800932022121827, + "tip_thickness": 34.67983561008608 + }, + "values": [ + 0.1059565469622612, + 75.57935333251953, + -8278.44921875 + ] + }, + { + "number": 4, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.13515426218509674, + 73.69579315185547, + -15871.068359375 + ] + }, + { + "number": 7, + "params": { + "support_angle": 29.09124836035503, + "tip_thickness": 35.50213529560301 + }, + "values": [ + 0.10616718232631683, + 75.49954986572266, + -8333.7919921875 + ] + }, + { + "number": 9, + "params": { + "support_angle": 41.59725093210579, + "tip_thickness": 38.736874205941255 + }, + "values": [ + 0.10606641322374344, + 77.42456817626953, + -8482.6328125 + ] + }, + { + "number": 10, + "params": { + "support_angle": 50.59264473611897, + "tip_thickness": 34.18481581956125 + }, + "values": [ + 0.11001653969287872, + 78.32686614990234, + -9909.66015625 + ] + }, + { + "number": 11, + "params": { + "support_angle": 34.60723242676091, + "tip_thickness": 40.99085529881075 + }, + "values": [ + 0.11470890045166016, + 71.76973724365234, + -10232.564453125 + ] + }, + { + "number": 12, + "params": { + "support_angle": 42.8034992108518, + "tip_thickness": 53.55527884179041 + }, + "values": [ + 0.1554829478263855, + 89.65568542480469, + -20128.802734375 + ] + }, + { + "number": 14, + "params": { + "support_angle": 49.620728443102124, + "tip_thickness": 31.393512381599933 + }, + "values": [ + 0.10854113101959229, + 78.32325744628906, + -9371.779296875 + ] + }, + { + "number": 15, + "params": { + "support_angle": 50.37724259507192, + "tip_thickness": 35.115723710618745 + }, + "values": [ + 0.11040062457323074, + 78.3082275390625, + -10054.8271484375 + ] + }, + { + "number": 17, + "params": { + "support_angle": 68.28160165372796, + "tip_thickness": 54.25192044349383 + }, + "values": [ + 0.15124832093715668, + 83.46127319335938, + -19232.740234375 + ] + }, + { + "number": 18, + "params": { + "support_angle": 35.23068845866854, + "tip_thickness": 32.93016342019152 + }, + "values": [ + 0.10423046350479126, + 77.35694122314453, + -7934.9453125 + ] + }, + { + "number": 24, + "params": { + "support_angle": 47.33551396716398, + "tip_thickness": 35.54563366576581 + }, + "values": [ + 0.10879749059677124, + 78.18163299560547, + -9440.0771484375 + ] + }, + { + "number": 25, + "params": { + "support_angle": 68.47923138822793, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.14725860953330994, + 82.43916320800781, + -18467.29296875 + ] + }, + { + "number": 26, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 56.844820512829465 + }, + "values": [ + 0.15847891569137573, + 86.1897201538086, + -20743.28515625 + ] + }, + { + "number": 27, + "params": { + "support_angle": 49.89499894055426, + "tip_thickness": 57.6562270506935 + }, + "values": [ + 0.1606408655643463, + 90.43415832519531, + -21159.50390625 + ] + }, + { + "number": 28, + "params": { + "support_angle": 24.424625102595975, + "tip_thickness": 35.87948587257436 + }, + "values": [ + 0.10864812880754471, + 73.66149139404297, + -8813.439453125 + ] + }, + { + "number": 30, + "params": { + "support_angle": 39.4338644844741, + "tip_thickness": 38.14047095321688 + }, + "values": [ + 0.10515307635068893, + 77.20490264892578, + -8183.75244140625 + ] + }, + { + "number": 37, + "params": { + "support_angle": 55.34286719238086, + "tip_thickness": 51.87021504122962 + }, + "values": [ + 0.14633406698703766, + 79.53317260742188, + -18268.1171875 + ] + }, + { + "number": 38, + "params": { + "support_angle": 58.56351733429729, + "tip_thickness": 32.22133955202271 + }, + "values": [ + 0.11347480118274689, + 77.44068145751953, + -11309.5009765625 + ] + }, + { + "number": 39, + "params": { + "support_angle": 37.923286427213625, + "tip_thickness": 33.47607178575389 + }, + "values": [ + 0.10462723672389984, + 77.50483703613281, + -8058.56103515625 + ] + }, + { + "number": 40, + "params": { + "support_angle": 63.155171293779674, + "tip_thickness": 48.69894380482674 + }, + "values": [ + 0.1325729489326477, + 81.2702407836914, + -15707.9404296875 + ] + }, + { + "number": 41, + "params": { + "support_angle": 36.54490124263246, + "tip_thickness": 31.90675050858071 + }, + "values": [ + 0.10441538691520691, + 77.51747131347656, + -7999.130859375 + ] + }, + { + "number": 42, + "params": { + "support_angle": 35.54911608578311, + "tip_thickness": 39.755499660802414 + }, + "values": [ + 0.10864792764186859, + 74.50569915771484, + -8988.908203125 + ] + }, + { + "number": 43, + "params": { + "support_angle": 56.480308916903205, + "tip_thickness": 49.12672414065639 + }, + "values": [ + 0.13565528392791748, + 80.51287841796875, + -16309.0615234375 + ] + }, + { + "number": 46, + "params": { + "support_angle": 58.03925243084487, + "tip_thickness": 46.83831592708489 + }, + "values": [ + 0.12997741997241974, + 81.35428619384766, + -15194.1845703125 + ] + }, + { + "number": 49, + "params": { + "support_angle": 21.27095633720476, + "tip_thickness": 33.23674280979913 + }, + "values": [ + 0.10700994729995728, + 74.54026794433594, + -8455.234375 + ] + }, + { + "number": 51, + "params": { + "support_angle": 37.923286427213625, + "tip_thickness": 39.4306794322898 + }, + "values": [ + 0.10637921094894409, + 76.08963775634766, + -8488.134765625 + ] + }, + { + "number": 52, + "params": { + "support_angle": 34.046725484369034, + "tip_thickness": 45.25712073494108 + }, + "values": [ + 0.12794305384159088, + 74.33392333984375, + -13282.2275390625 + ] + }, + { + "number": 54, + "params": { + "support_angle": 32.464611457443745, + "tip_thickness": 42.31148769106889 + }, + "values": [ + 0.12083136290311813, + 71.6664810180664, + -11710.95703125 + ] + }, + { + "number": 56, + "params": { + "support_angle": 24.424625102595975, + "tip_thickness": 35.87948587257436 + }, + "values": [ + 0.10864812880754471, + 73.66149139404297, + -8813.439453125 + ] + }, + { + "number": 57, + "params": { + "support_angle": 31.43990827458112, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.1041165217757225, + 76.9697494506836, + -7904.35400390625 + ] + }, + { + "number": 59, + "params": { + "support_angle": 29.09124836035503, + "tip_thickness": 40.99085529881075 + }, + "values": [ + 0.11795789003372192, + 70.98284912109375, + -11055.01953125 + ] + }, + { + "number": 60, + "params": { + "support_angle": 63.155171293779674, + "tip_thickness": 48.69894380482674 + }, + "values": [ + 0.1325729489326477, + 81.2702407836914, + -15707.9404296875 + ] + }, + { + "number": 62, + "params": { + "support_angle": 66.48488261712865, + "tip_thickness": 54.24361138693251 + }, + "values": [ + 0.15239883959293365, + 83.6440200805664, + -19463.36328125 + ] + }, + { + "number": 63, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 31.90675050858071 + }, + "values": [ + 0.11520456522703171, + 76.67537689208984, + -11996.55859375 + ] + }, + { + "number": 64, + "params": { + "support_angle": 51.67018782552117, + "tip_thickness": 56.143817705631534 + }, + "values": [ + 0.15984582901000977, + 89.11952209472656, + -20942.8515625 + ] + }, + { + "number": 65, + "params": { + "support_angle": 60.18360384495573, + "tip_thickness": 35.597101766581076 + }, + "values": [ + 0.11919906735420227, + 75.10107421875, + -13256.1005859375 + ] + }, + { + "number": 66, + "params": { + "support_angle": 64.62794992449889, + "tip_thickness": 46.180267257469524 + }, + "values": [ + 0.12979814410209656, + 81.6006088256836, + -15297.7236328125 + ] + }, + { + "number": 67, + "params": { + "support_angle": 60.372007758203125, + "tip_thickness": 56.8827389977048 + }, + "values": [ + 0.16017363965511322, + 86.90072631835938, + -21086.689453125 + ] + }, + { + "number": 68, + "params": { + "support_angle": 35.90017374859319, + "tip_thickness": 33.3015577358303 + }, + "values": [ + 0.10430055111646652, + 77.38353729248047, + -7958.01708984375 + ] + }, + { + "number": 69, + "params": { + "support_angle": 35.58555380447055, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11189951002597809, + 72.98871612548828, + -9688.0703125 + ] + }, + { + "number": 70, + "params": { + "support_angle": 27.800932022121827, + "tip_thickness": 34.67983561008608 + }, + "values": [ + 0.1059565469622612, + 75.57935333251953, + -8278.44921875 + ] + }, + { + "number": 71, + "params": { + "support_angle": 31.396758127097083, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11595742404460907, + 71.04383087158203, + -10518.568359375 + ] + }, + { + "number": 72, + "params": { + "support_angle": 50.59264473611897, + "tip_thickness": 32.22133955202271 + }, + "values": [ + 0.10920578241348267, + 78.35645294189453, + -9617.990234375 + ] + }, + { + "number": 76, + "params": { + "support_angle": 46.1366414690997, + "tip_thickness": 55.82191749769031 + }, + "values": [ + 0.15914370119571686, + 90.37212371826172, + -20863.837890625 + ] + }, + { + "number": 81, + "params": { + "support_angle": 31.105390523536514, + "tip_thickness": 33.595961020010485 + }, + "values": [ + 0.10451866686344147, + 76.65894317626953, + -7993.1142578125 + ] + }, + { + "number": 82, + "params": { + "support_angle": 22.261364445526905, + "tip_thickness": 32.22133955202271 + }, + "values": [ + 0.10596974194049835, + 75.34008026123047, + -8244.9931640625 + ] + }, + { + "number": 83, + "params": { + "support_angle": 58.03925243084487, + "tip_thickness": 46.83831592708489 + }, + "values": [ + 0.12997741997241974, + 81.35428619384766, + -15194.1845703125 + ] + }, + { + "number": 85, + "params": { + "support_angle": 36.8807585701814, + "tip_thickness": 58.287291117375574 + }, + "values": [ + 0.15261533856391907, + 90.24761199951172, + -19521.34375 + ] + }, + { + "number": 86, + "params": { + "support_angle": 36.160146601037766, + "tip_thickness": 45.563718652300985 + }, + "values": [ + 0.1281682699918747, + 72.67020416259766, + -13502.1982421875 + ] + }, + { + "number": 87, + "params": { + "support_angle": 35.54911608578311, + "tip_thickness": 39.755499660802414 + }, + "values": [ + 0.10864792764186859, + 74.50569915771484, + -8988.908203125 + ] + }, + { + "number": 89, + "params": { + "support_angle": 68.58910413604804, + "tip_thickness": 58.87341884826334 + }, + "values": [ + 0.16115634143352509, + 87.3917007446289, + -21306.380859375 + ] + }, + { + "number": 92, + "params": { + "support_angle": 35.04391549083849, + "tip_thickness": 38.54521483132403 + }, + "values": [ + 0.1066935807466507, + 75.56289672851562, + -8529.873046875 + ] + }, + { + "number": 94, + "params": { + "support_angle": 37.923286427213625, + "tip_thickness": 33.47607178575389 + }, + "values": [ + 0.10462723672389984, + 77.50483703613281, + -8058.56103515625 + ] + }, + { + "number": 96, + "params": { + "support_angle": 22.573937562499466, + "tip_thickness": 38.359393927098346 + }, + "values": [ + 0.1143990308046341, + 71.06428527832031, + -10081.29296875 + ] + }, + { + "number": 97, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 37.186856720009175 + }, + "values": [ + 0.12223504483699799, + 76.6735610961914, + -14162.1201171875 + ] + }, + { + "number": 99, + "params": { + "support_angle": 21.27095633720476, + "tip_thickness": 33.23674280979913 + }, + "values": [ + 0.10700994729995728, + 74.54026794433594, + -8455.234375 + ] + }, + { + "number": 100, + "params": { + "support_angle": 31.396758127097083, + "tip_thickness": 34.346846162736696 + }, + "values": [ + 0.10474885255098343, + 76.51874542236328, + -8047.7216796875 + ] + }, + { + "number": 101, + "params": { + "support_angle": 44.472638013878154, + "tip_thickness": 47.959754525911094 + }, + "values": [ + 0.13215340673923492, + 74.97693634033203, + -15103.546875 + ] + }, + { + "number": 102, + "params": { + "support_angle": 69.28252270553003, + "tip_thickness": 42.52233009446337 + }, + "values": [ + 0.12779651582241058, + 81.55487823486328, + -15195.6435546875 + ] + }, + { + "number": 103, + "params": { + "support_angle": 58.56351733429729, + "tip_thickness": 54.25192044349383 + }, + "values": [ + 0.15588942170143127, + 84.2198257446289, + -20130.296875 + ] + }, + { + "number": 105, + "params": { + "support_angle": 53.606777370293926, + "tip_thickness": 52.84858845986153 + }, + "values": [ + 0.1483200490474701, + 79.95873260498047, + -18539.56640625 + ] + }, + { + "number": 106, + "params": { + "support_angle": 68.47923138822793, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.14725860953330994, + 82.43916320800781, + -18467.29296875 + ] + }, + { + "number": 107, + "params": { + "support_angle": 31.881877199619986, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11571356654167175, + 71.12530517578125, + -10460.037109375 + ] + }, + { + "number": 109, + "params": { + "support_angle": 68.28160165372796, + "tip_thickness": 51.84649045835579 + }, + "values": [ + 0.14097896218299866, + 81.59192657470703, + -17449.361328125 + ] + }, + { + "number": 111, + "params": { + "support_angle": 51.61529152967897, + "tip_thickness": 31.393512381599933 + }, + "values": [ + 0.1093631461262703, + 78.38308715820312, + -9680.71875 + ] + }, + { + "number": 113, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 32.70869310163225 + }, + "values": [ + 0.11606132239103317, + 76.30424499511719, + -12345.7021484375 + ] + }, + { + "number": 114, + "params": { + "support_angle": 61.7651247794619, + "tip_thickness": 39.623401949152075 + }, + "values": [ + 0.12402036786079407, + 78.7585678100586, + -14538.037109375 + ] + }, + { + "number": 115, + "params": { + "support_angle": 29.325925519992712, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11698425561189651, + 70.90274810791016, + -10788.1611328125 + ] + }, + { + "number": 116, + "params": { + "support_angle": 46.1366414690997, + "tip_thickness": 31.223254246642917 + }, + "values": [ + 0.10717146098613739, + 78.07367706298828, + -8880.228515625 + ] + }, + { + "number": 117, + "params": { + "support_angle": 49.544647159412094, + "tip_thickness": 50.32693085526847 + }, + "values": [ + 0.13420960307121277, + 73.84709930419922, + -15595.884765625 + ] + }, + { + "number": 118, + "params": { + "support_angle": 37.923286427213625, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.10465380549430847, + 77.53772735595703, + -8069.35546875 + ] + }, + { + "number": 121, + "params": { + "support_angle": 31.324788759896897, + "tip_thickness": 38.359393927098346 + }, + "values": [ + 0.10881928354501724, + 73.9853744506836, + -8930.0888671875 + ] + }, + { + "number": 123, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.13515426218509674, + 73.69579315185547, + -15871.068359375 + ] + }, + { + "number": 127, + "params": { + "support_angle": 66.83649943683673, + "tip_thickness": 47.959754525911094 + }, + "values": [ + 0.13140633702278137, + 81.47834014892578, + -15540.177734375 + ] + }, + { + "number": 128, + "params": { + "support_angle": 51.67018782552117, + "tip_thickness": 35.597101766581076 + }, + "values": [ + 0.1115550547838211, + 78.25357818603516, + -10497.4228515625 + ] + }, + { + "number": 129, + "params": { + "support_angle": 26.876047207299663, + "tip_thickness": 34.18481581956125 + }, + "values": [ + 0.10589270293712616, + 75.5810775756836, + -8257.482421875 + ] + }, + { + "number": 132, + "params": { + "support_angle": 66.23468091392814, + "tip_thickness": 51.39734361668985 + }, + "values": [ + 0.14031454920768738, + 81.38665771484375, + -17341.248046875 + ] + }, + { + "number": 135, + "params": { + "support_angle": 60.86111001006079, + "tip_thickness": 46.28088249474746 + }, + "values": [ + 0.12956662476062775, + 81.53195190429688, + -15209.73046875 + ] + }, + { + "number": 137, + "params": { + "support_angle": 46.48252891780032, + "tip_thickness": 37.255568727013554 + }, + "values": [ + 0.10905493795871735, + 78.09574890136719, + -9519.95703125 + ] + }, + { + "number": 139, + "params": { + "support_angle": 65.02090285816652, + "tip_thickness": 48.99304371819804 + }, + "values": [ + 0.13310015201568604, + 81.30635070800781, + -15817.5986328125 + ] + }, + { + "number": 140, + "params": { + "support_angle": 36.951489552435035, + "tip_thickness": 40.476287238379825 + }, + "values": [ + 0.10941559076309204, + 74.3121337890625, + -9207.439453125 + ] + }, + { + "number": 141, + "params": { + "support_angle": 56.297783943511966, + "tip_thickness": 56.913307798577314 + }, + "values": [ + 0.15990939736366272, + 87.3863296508789, + -20940.859375 + ] + }, + { + "number": 142, + "params": { + "support_angle": 64.35432121325587, + "tip_thickness": 53.396266375728715 + }, + "values": [ + 0.15078045427799225, + 82.89861297607422, + -19140.689453125 + ] + }, + { + "number": 143, + "params": { + "support_angle": 52.10158230771439, + "tip_thickness": 47.959754525911094 + }, + "values": [ + 0.1326238065958023, + 80.19908142089844, + -15425.2060546875 + ] + }, + { + "number": 144, + "params": { + "support_angle": 68.47923138822793, + "tip_thickness": 32.52419894985147 + }, + "values": [ + 0.1201542541384697, + 74.55197143554688, + -13594.1865234375 + ] + }, + { + "number": 145, + "params": { + "support_angle": 55.34286719238086, + "tip_thickness": 34.84886142283841 + }, + "values": [ + 0.11369557678699493, + 77.50555419921875, + -11374.208984375 + ] + }, + { + "number": 146, + "params": { + "support_angle": 64.92770942635397, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12626515328884125, + 80.85123443603516, + -14946.7060546875 + ] + }, + { + "number": 147, + "params": { + "support_angle": 50.321452982979494, + "tip_thickness": 30.27591154849889 + }, + "values": [ + 0.10852932929992676, + 78.3441390991211, + -9373.033203125 + ] + }, + { + "number": 148, + "params": { + "support_angle": 29.09124836035503, + "tip_thickness": 33.044146285980965 + }, + "values": [ + 0.1047094538807869, + 76.46110534667969, + -8025.044921875 + ] + }, + { + "number": 151, + "params": { + "support_angle": 36.8807585701814, + "tip_thickness": 30.15184751538656 + }, + "values": [ + 0.1045205295085907, + 77.5667724609375, + -8033.56982421875 + ] + }, + { + "number": 152, + "params": { + "support_angle": 49.544647159412094, + "tip_thickness": 50.32693085526847 + }, + "values": [ + 0.13420960307121277, + 73.84709930419922, + -15595.884765625 + ] + }, + { + "number": 153, + "params": { + "support_angle": 36.951489552435035, + "tip_thickness": 34.82424154252496 + }, + "values": [ + 0.1044565886259079, + 77.34011840820312, + -7997.97998046875 + ] + }, + { + "number": 154, + "params": { + "support_angle": 64.35432121325587, + "tip_thickness": 53.396266375728715 + }, + "values": [ + 0.15078045427799225, + 82.89861297607422, + -19140.689453125 + ] + }, + { + "number": 155, + "params": { + "support_angle": 61.7651247794619, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.15184669196605682, + 82.83464050292969, + -19344.71484375 + ] + }, + { + "number": 156, + "params": { + "support_angle": 47.436689468329305, + "tip_thickness": 49.12672414065639 + }, + "values": [ + 0.1322527527809143, + 74.17280578613281, + -15216.466796875 + ] + }, + { + "number": 157, + "params": { + "support_angle": 66.23468091392814, + "tip_thickness": 51.39734361668985 + }, + "values": [ + 0.14031454920768738, + 81.38665771484375, + -17341.248046875 + ] + }, + { + "number": 158, + "params": { + "support_angle": 54.59475988463466, + "tip_thickness": 49.55883778507801 + }, + "values": [ + 0.13909828662872314, + 80.07440185546875, + -16956.29296875 + ] + }, + { + "number": 161, + "params": { + "support_angle": 31.862454374840006, + "tip_thickness": 39.761990944778034 + }, + "values": [ + 0.11244813352823257, + 72.25245666503906, + -9687.2216796875 + ] + }, + { + "number": 163, + "params": { + "support_angle": 52.48164495236073, + "tip_thickness": 51.84649045835579 + }, + "values": [ + 0.14125052094459534, + 76.38086700439453, + -17176.599609375 + ] + }, + { + "number": 165, + "params": { + "support_angle": 52.88064461501717, + "tip_thickness": 35.597101766581076 + }, + "values": [ + 0.11249491572380066, + 78.03231048583984, + -10880.0166015625 + ] + }, + { + "number": 167, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 32.81024303484278 + }, + "values": [ + 0.11891955137252808, + 74.93551635742188, + -13244.125 + ] + }, + { + "number": 168, + "params": { + "support_angle": 38.38579015297168, + "tip_thickness": 37.95607103045177 + }, + "values": [ + 0.10504335165023804, + 76.99024963378906, + -8147.57568359375 + ] + }, + { + "number": 169, + "params": { + "support_angle": 32.19948216895418, + "tip_thickness": 59.190316642573364 + }, + "values": [ + 0.14833776652812958, + 89.8187484741211, + -18553.626953125 + ] + }, + { + "number": 170, + "params": { + "support_angle": 39.65488623333802, + "tip_thickness": 56.844820512829465 + }, + "values": [ + 0.15461842715740204, + 90.31965637207031, + -19954.19140625 + ] + }, + { + "number": 171, + "params": { + "support_angle": 68.47923138822793, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.14725860953330994, + 82.43916320800781, + -18467.29296875 + ] + }, + { + "number": 172, + "params": { + "support_angle": 64.60232775885567, + "tip_thickness": 48.93415877991789 + }, + "values": [ + 0.13299737870693207, + 81.29732513427734, + -15796.65234375 + ] + }, + { + "number": 173, + "params": { + "support_angle": 35.21211214797688, + "tip_thickness": 56.844820512829465 + }, + "values": [ + 0.1492553949356079, + 89.71302795410156, + -18740.94140625 + ] + }, + { + "number": 175, + "params": { + "support_angle": 29.325925519992712, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11698425561189651, + 70.90274810791016, + -10788.1611328125 + ] + }, + { + "number": 177, + "params": { + "support_angle": 36.8807585701814, + "tip_thickness": 46.28088249474746 + }, + "values": [ + 0.1297694444656372, + 71.82013702392578, + -14099.5439453125 + ] + }, + { + "number": 178, + "params": { + "support_angle": 68.47923138822793, + "tip_thickness": 35.857289633941335 + }, + "values": [ + 0.12192479521036148, + 76.23880767822266, + -14111.9716796875 + ] + }, + { + "number": 181, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 49.364168877215036 + }, + "values": [ + 0.13481184840202332, + 80.9857406616211, + -16195.2822265625 + ] + }, + { + "number": 182, + "params": { + "support_angle": 46.1366414690997, + "tip_thickness": 31.223254246642917 + }, + "values": [ + 0.10717146098613739, + 78.07367706298828, + -8880.228515625 + ] + }, + { + "number": 183, + "params": { + "support_angle": 28.855533970352447, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11723171174526215, + 70.90250396728516, + -10856.4140625 + ] + }, + { + "number": 184, + "params": { + "support_angle": 67.02292921764572, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.1612410843372345, + 87.39888763427734, + -21318.8125 + ] + }, + { + "number": 185, + "params": { + "support_angle": 49.544647159412094, + "tip_thickness": 58.287291117375574 + }, + "values": [ + 0.1607365906238556, + 90.64505004882812, + -21170.751953125 + ] + }, + { + "number": 186, + "params": { + "support_angle": 67.69642885012937, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12664075195789337, + 81.06171417236328, + -15034.86328125 + ] + }, + { + "number": 187, + "params": { + "support_angle": 36.8807585701814, + "tip_thickness": 58.287291117375574 + }, + "values": [ + 0.15261533856391907, + 90.24761199951172, + -19521.34375 + ] + }, + { + "number": 188, + "params": { + "support_angle": 65.74321951102243, + "tip_thickness": 45.563718652300985 + }, + "values": [ + 0.1295650154352188, + 81.66310119628906, + -15301.607421875 + ] + }, + { + "number": 189, + "params": { + "support_angle": 38.50793501277222, + "tip_thickness": 30.463698495866023 + }, + "values": [ + 0.10481756925582886, + 77.58677673339844, + -8123.30810546875 + ] + }, + { + "number": 192, + "params": { + "support_angle": 37.923286427213625, + "tip_thickness": 39.4306794322898 + }, + "values": [ + 0.10637921094894409, + 76.08963775634766, + -8488.134765625 + ] + }, + { + "number": 194, + "params": { + "support_angle": 68.18099885446264, + "tip_thickness": 55.5902836640208 + }, + "values": [ + 0.15500371158123016, + 84.84846496582031, + -20031.328125 + ] + }, + { + "number": 195, + "params": { + "support_angle": 26.876047207299663, + "tip_thickness": 34.18481581956125 + }, + "values": [ + 0.10589270293712616, + 75.5810775756836, + -8257.482421875 + ] + }, + { + "number": 196, + "params": { + "support_angle": 34.72244460347928, + "tip_thickness": 58.466566117599996 + }, + "values": [ + 0.15038873255252838, + 90.0067138671875, + -19026.119140625 + ] + }, + { + "number": 198, + "params": { + "support_angle": 62.556833575842845, + "tip_thickness": 39.50766015468833 + }, + "values": [ + 0.12418977171182632, + 78.88951873779297, + -14583.3505859375 + ] + }, + { + "number": 200, + "params": { + "support_angle": 68.47923138822793, + "tip_thickness": 46.70403787375051 + }, + "values": [ + 0.13037247955799103, + 81.6016845703125, + -15413.7001953125 + ] + }, + { + "number": 201, + "params": { + "support_angle": 66.80773870803904, + "tip_thickness": 42.81323365878769 + }, + "values": [ + 0.12780505418777466, + 81.55426025390625, + -15175.732421875 + ] + }, + { + "number": 203, + "params": { + "support_angle": 48.50305850446825, + "tip_thickness": 32.915294813123054 + }, + "values": [ + 0.10848382860422134, + 78.27499389648438, + -9339.78125 + ] + }, + { + "number": 206, + "params": { + "support_angle": 45.91648261818683, + "tip_thickness": 56.32119215783867 + }, + "values": [ + 0.15910828113555908, + 90.46391296386719, + -20856.7109375 + ] + }, + { + "number": 208, + "params": { + "support_angle": 29.09124836035503, + "tip_thickness": 40.99085529881075 + }, + "values": [ + 0.11795789003372192, + 70.98284912109375, + -11055.01953125 + ] + }, + { + "number": 209, + "params": { + "support_angle": 49.544647159412094, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.1454390287399292, + 79.58540344238281, + -17924.70703125 + ] + }, + { + "number": 211, + "params": { + "support_angle": 56.12260576307526, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.16086003184318542, + 88.87126922607422, + -21196.6953125 + ] + }, + { + "number": 212, + "params": { + "support_angle": 55.12420419935546, + "tip_thickness": 51.87021504122962 + }, + "values": [ + 0.14607040584087372, + 79.34710693359375, + -18215.25 + ] + }, + { + "number": 213, + "params": { + "support_angle": 36.951489552435035, + "tip_thickness": 40.78473453659265 + }, + "values": [ + 0.11051151156425476, + 73.80848693847656, + -9455.2626953125 + ] + }, + { + "number": 215, + "params": { + "support_angle": 66.48488261712865, + "tip_thickness": 54.28083466435541 + }, + "values": [ + 0.15251636505126953, + 83.68587493896484, + -19490.302734375 + ] + }, + { + "number": 216, + "params": { + "support_angle": 60.50566973395904, + "tip_thickness": 56.01216955740311 + }, + "values": [ + 0.15903252363204956, + 86.08209228515625, + -20838.578125 + ] + }, + { + "number": 219, + "params": { + "support_angle": 35.58555380447055, + "tip_thickness": 45.04548884061599 + }, + "values": [ + 0.12755583226680756, + 73.73133850097656, + -13211.1220703125 + ] + }, + { + "number": 220, + "params": { + "support_angle": 62.556833575842845, + "tip_thickness": 53.948855369003255 + }, + "values": [ + 0.15378427505493164, + 83.72016143798828, + -19733.775390625 + ] + }, + { + "number": 221, + "params": { + "support_angle": 52.49819653888826, + "tip_thickness": 51.059006317731104 + }, + "values": [ + 0.1400517374277115, + 76.39205932617188, + -16919.052734375 + ] + }, + { + "number": 222, + "params": { + "support_angle": 68.28160165372796, + "tip_thickness": 58.287291117375574 + }, + "values": [ + 0.1604105830192566, + 87.06126403808594, + -21156.16015625 + ] + }, + { + "number": 223, + "params": { + "support_angle": 59.789633471805054, + "tip_thickness": 54.24361138693251 + }, + "values": [ + 0.15570932626724243, + 84.23917388916016, + -20115.0 + ] + }, + { + "number": 225, + "params": { + "support_angle": 37.923286427213625, + "tip_thickness": 32.81024303484278 + }, + "values": [ + 0.10464169830083847, + 77.52526092529297, + -8064.54248046875 + ] + }, + { + "number": 226, + "params": { + "support_angle": 38.779147631997205, + "tip_thickness": 32.81945819522607 + }, + "values": [ + 0.10482889413833618, + 77.54039001464844, + -8121.00146484375 + ] + }, + { + "number": 229, + "params": { + "support_angle": 34.72244460347928, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.10417717695236206, + 77.36528015136719, + -7920.83984375 + ] + }, + { + "number": 230, + "params": { + "support_angle": 58.56351733429729, + "tip_thickness": 32.22133955202271 + }, + "values": [ + 0.11347480118274689, + 77.44068145751953, + -11309.5009765625 + ] + }, + { + "number": 231, + "params": { + "support_angle": 48.9140070498087, + "tip_thickness": 31.078268213902263 + }, + "values": [ + 0.10817606002092361, + 78.2893295288086, + -9237.888671875 + ] + }, + { + "number": 233, + "params": { + "support_angle": 52.48164495236073, + "tip_thickness": 51.84649045835579 + }, + "values": [ + 0.14125052094459534, + 76.38086700439453, + -17176.599609375 + ] + }, + { + "number": 238, + "params": { + "support_angle": 34.327062606414216, + "tip_thickness": 37.186856720009175 + }, + "values": [ + 0.10555028915405273, + 76.16165161132812, + -8255.6064453125 + ] + }, + { + "number": 239, + "params": { + "support_angle": 61.7651247794619, + "tip_thickness": 47.724997817070324 + }, + "values": [ + 0.13110800087451935, + 81.35581970214844, + -15422.302734375 + ] + }, + { + "number": 243, + "params": { + "support_angle": 26.35302563259424, + "tip_thickness": 40.476287238379825 + }, + "values": [ + 0.11765411496162415, + 71.02568817138672, + -10967.4609375 + ] + }, + { + "number": 244, + "params": { + "support_angle": 37.05331755251292, + "tip_thickness": 32.81024303484278 + }, + "values": [ + 0.1044781431555748, + 77.50143432617188, + -8015.52880859375 + ] + }, + { + "number": 245, + "params": { + "support_angle": 67.02292921764572, + "tip_thickness": 45.66729780164413 + }, + "values": [ + 0.12969094514846802, + 81.65805053710938, + -15329.912109375 + ] + }, + { + "number": 246, + "params": { + "support_angle": 58.499677654930544, + "tip_thickness": 51.84649045835579 + }, + "values": [ + 0.1470905840396881, + 80.95133972167969, + -18428.642578125 + ] + }, + { + "number": 247, + "params": { + "support_angle": 58.56351733429729, + "tip_thickness": 54.25192044349383 + }, + "values": [ + 0.15588942170143127, + 84.2198257446289, + -20130.296875 + ] + }, + { + "number": 249, + "params": { + "support_angle": 58.56351733429729, + "tip_thickness": 32.56042394981304 + }, + "values": [ + 0.11380836367607117, + 77.31302642822266, + -11435.486328125 + ] + }, + { + "number": 251, + "params": { + "support_angle": 22.584086058430387, + "tip_thickness": 32.81024303484278 + }, + "values": [ + 0.10626446455717087, + 75.12947845458984, + -8306.54296875 + ] + }, + { + "number": 252, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 45.94063894704444 + }, + "values": [ + 0.12936925888061523, + 81.5841293334961, + -15196.5908203125 + ] + }, + { + "number": 253, + "params": { + "support_angle": 56.480308916903205, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.16093726456165314, + 88.78457641601562, + -21212.0234375 + ] + }, + { + "number": 255, + "params": { + "support_angle": 47.031756080505325, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.15735994279384613, + 88.70494079589844, + -20442.20703125 + ] + }, + { + "number": 256, + "params": { + "support_angle": 51.87149507491033, + "tip_thickness": 51.78274001167985 + }, + "values": [ + 0.1397484540939331, + 75.62481689453125, + -16853.125 + ] + }, + { + "number": 257, + "params": { + "support_angle": 68.79260397312673, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.12010729312896729, + 74.5454330444336, + -13580.4208984375 + ] + }, + { + "number": 258, + "params": { + "support_angle": 39.65488623333802, + "tip_thickness": 45.48901044903586 + }, + "values": [ + 0.12851136922836304, + 71.92572021484375, + -13918.9033203125 + ] + }, + { + "number": 259, + "params": { + "support_angle": 56.480308916903205, + "tip_thickness": 49.12672414065639 + }, + "values": [ + 0.13565528392791748, + 80.51287841796875, + -16309.0615234375 + ] + }, + { + "number": 261, + "params": { + "support_angle": 34.046725484369034, + "tip_thickness": 38.124967537862226 + }, + "values": [ + 0.10669313371181488, + 75.47348022460938, + -8511.208984375 + ] + }, + { + "number": 262, + "params": { + "support_angle": 41.9485710352818, + "tip_thickness": 54.25192044349383 + }, + "values": [ + 0.15524697303771973, + 89.96098327636719, + -20093.576171875 + ] + }, + { + "number": 263, + "params": { + "support_angle": 58.56351733429729, + "tip_thickness": 32.35369144026798 + }, + "values": [ + 0.11360302567481995, + 77.3915023803711, + -11357.966796875 + ] + }, + { + "number": 264, + "params": { + "support_angle": 52.49819653888826, + "tip_thickness": 51.059006317731104 + }, + "values": [ + 0.1400517374277115, + 76.39205932617188, + -16919.052734375 + ] + }, + { + "number": 266, + "params": { + "support_angle": 60.18360384495573, + "tip_thickness": 30.760522302463727 + }, + "values": [ + 0.11328000575304031, + 77.4607925415039, + -11242.078125 + ] + }, + { + "number": 267, + "params": { + "support_angle": 68.13242073389625, + "tip_thickness": 46.83831592708489 + }, + "values": [ + 0.13045331835746765, + 81.58904266357422, + -15421.5654296875 + ] + }, + { + "number": 268, + "params": { + "support_angle": 61.79900602561029, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.15182732045650482, + 82.83250427246094, + -19341.3203125 + ] + }, + { + "number": 270, + "params": { + "support_angle": 31.324788759896897, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11599356681108475, + 71.03447723388672, + -10527.2490234375 + ] + }, + { + "number": 271, + "params": { + "support_angle": 40.44764722071349, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.10725622624158859, + 76.32754516601562, + -8758.5986328125 + ] + }, + { + "number": 272, + "params": { + "support_angle": 39.65488623333802, + "tip_thickness": 35.961470446025174 + }, + "values": [ + 0.10502772033214569, + 77.45445251464844, + -8170.1884765625 + ] + }, + { + "number": 273, + "params": { + "support_angle": 68.28160165372796, + "tip_thickness": 54.25192044349383 + }, + "values": [ + 0.15124832093715668, + 83.46127319335938, + -19232.740234375 + ] + }, + { + "number": 274, + "params": { + "support_angle": 34.72244460347928, + "tip_thickness": 35.19882960212537 + }, + "values": [ + 0.10444585978984833, + 76.91856384277344, + -7989.04443359375 + ] + }, + { + "number": 275, + "params": { + "support_angle": 27.8218521335543, + "tip_thickness": 37.50728694493786 + }, + "values": [ + 0.10949549078941345, + 73.36516571044922, + -9029.166015625 + ] + }, + { + "number": 277, + "params": { + "support_angle": 55.72979613500312, + "tip_thickness": 49.80592130153194 + }, + "values": [ + 0.1397808939218521, + 80.20856475830078, + -17134.81640625 + ] + }, + { + "number": 278, + "params": { + "support_angle": 33.99669484729714, + "tip_thickness": 32.56042394981304 + }, + "values": [ + 0.10412964969873428, + 77.26203155517578, + -7907.62353515625 + ] + }, + { + "number": 279, + "params": { + "support_angle": 67.7432640331597, + "tip_thickness": 31.223254246642917 + }, + "values": [ + 0.11845505982637405, + 75.02986907958984, + -13136.521484375 + ] + }, + { + "number": 280, + "params": { + "support_angle": 56.89484583478843, + "tip_thickness": 32.56042394981304 + }, + "values": [ + 0.11260666698217392, + 77.82437896728516, + -10962.5029296875 + ] + }, + { + "number": 281, + "params": { + "support_angle": 47.717702625570034, + "tip_thickness": 48.35162238703057 + }, + "values": [ + 0.13126005232334137, + 75.46430969238281, + -14933.3740234375 + ] + }, + { + "number": 282, + "params": { + "support_angle": 40.980003121389494, + "tip_thickness": 38.359393927098346 + }, + "values": [ + 0.10568375140428543, + 77.40196990966797, + -8357.107421875 + ] + }, + { + "number": 283, + "params": { + "support_angle": 32.386549475057876, + "tip_thickness": 37.186856720009175 + }, + "values": [ + 0.10641810297966003, + 75.52481079101562, + -8425.8095703125 + ] + }, + { + "number": 286, + "params": { + "support_angle": 20.719674431487793, + "tip_thickness": 33.48217921520749 + }, + "values": [ + 0.10745000839233398, + 74.24906158447266, + -8543.333984375 + ] + }, + { + "number": 287, + "params": { + "support_angle": 22.300132101087637, + "tip_thickness": 31.221864069569104 + }, + "values": [ + 0.10537870228290558, + 75.7894287109375, + -8126.20751953125 + ] + }, + { + "number": 288, + "params": { + "support_angle": 62.77302920055036, + "tip_thickness": 46.83831592708489 + }, + "values": [ + 0.13018973171710968, + 81.49352264404297, + -15317.31640625 + ] + }, + { + "number": 291, + "params": { + "support_angle": 43.67358853902829, + "tip_thickness": 35.196056097300456 + }, + "values": [ + 0.10674355924129486, + 77.78915405273438, + -8719.5703125 + ] + }, + { + "number": 292, + "params": { + "support_angle": 49.544647159412094, + "tip_thickness": 56.8827389977048 + }, + "values": [ + 0.16034191846847534, + 90.24244689941406, + -21102.4921875 + ] + }, + { + "number": 293, + "params": { + "support_angle": 38.779147631997205, + "tip_thickness": 43.01554947713919 + }, + "values": [ + 0.1213734969496727, + 71.38740539550781, + -11917.83203125 + ] + }, + { + "number": 294, + "params": { + "support_angle": 39.92523671986867, + "tip_thickness": 48.47550294156649 + }, + "values": [ + 0.13217513263225555, + 76.1971435546875, + -15239.931640625 + ] + }, + { + "number": 295, + "params": { + "support_angle": 51.75468254338219, + "tip_thickness": 32.56042394981304 + }, + "values": [ + 0.109856516122818, + 78.35123443603516, + -9858.1220703125 + ] + }, + { + "number": 297, + "params": { + "support_angle": 61.7651247794619, + "tip_thickness": 31.359120293161336 + }, + "values": [ + 0.11487977206707001, + 76.79535675048828, + -11867.8505859375 + ] + }, + { + "number": 298, + "params": { + "support_angle": 38.73063073132356, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.10832511633634567, + 75.22560119628906, + -9007.20703125 + ] + }, + { + "number": 300, + "params": { + "support_angle": 45.15681292900439, + "tip_thickness": 32.56042394981304 + }, + "values": [ + 0.10699822753667831, + 77.9807357788086, + -8812.1396484375 + ] + }, + { + "number": 301, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 49.760808948568354 + }, + "values": [ + 0.13568587601184845, + 81.0245361328125, + -16398.44140625 + ] + }, + { + "number": 302, + "params": { + "support_angle": 28.146721354071484, + "tip_thickness": 32.11706242201289 + }, + "values": [ + 0.1045612171292305, + 76.53260803222656, + -7986.1611328125 + ] + }, + { + "number": 304, + "params": { + "support_angle": 52.12096391031578, + "tip_thickness": 30.795339316248654 + }, + "values": [ + 0.1093699112534523, + 78.38938903808594, + -9685.984375 + ] + }, + { + "number": 305, + "params": { + "support_angle": 49.28877906367316, + "tip_thickness": 51.87021504122962 + }, + "values": [ + 0.14244265854358673, + 78.02801513671875, + -17364.94140625 + ] + }, + { + "number": 306, + "params": { + "support_angle": 61.7651247794619, + "tip_thickness": 58.20690724274873 + }, + "values": [ + 0.16142278909683228, + 87.68589782714844, + -21352.6328125 + ] + }, + { + "number": 307, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 31.223254246642917 + }, + "values": [ + 0.1145285815000534, + 76.94524383544922, + -11728.353515625 + ] + }, + { + "number": 308, + "params": { + "support_angle": 49.544647159412094, + "tip_thickness": 47.26422533627637 + }, + "values": [ + 0.13088226318359375, + 79.55110168457031, + -14883.640625 + ] + }, + { + "number": 309, + "params": { + "support_angle": 64.92770942635397, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12626515328884125, + 80.85123443603516, + -14946.7060546875 + ] + }, + { + "number": 313, + "params": { + "support_angle": 39.30513189003871, + "tip_thickness": 58.466566117599996 + }, + "values": [ + 0.15515853464603424, + 90.54681396484375, + -20069.17578125 + ] + }, + { + "number": 314, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 58.83571691471742 + }, + "values": [ + 0.16174301505088806, + 88.0146255493164, + -21418.71484375 + ] + }, + { + "number": 315, + "params": { + "support_angle": 65.26753209780318, + "tip_thickness": 35.873734043678894 + }, + "values": [ + 0.12109647691249847, + 75.32166290283203, + -13863.1708984375 + ] + }, + { + "number": 317, + "params": { + "support_angle": 36.951489552435035, + "tip_thickness": 32.080839026254964 + }, + "values": [ + 0.10447525233030319, + 77.52490234375, + -8016.2607421875 + ] + }, + { + "number": 318, + "params": { + "support_angle": 61.7651247794619, + "tip_thickness": 39.623401949152075 + }, + "values": [ + 0.12402036786079407, + 78.7585678100586, + -14538.037109375 + ] + }, + { + "number": 319, + "params": { + "support_angle": 55.12420419935546, + "tip_thickness": 51.87021504122962 + }, + "values": [ + 0.14607040584087372, + 79.34710693359375, + -18215.25 + ] + }, + { + "number": 321, + "params": { + "support_angle": 60.372007758203125, + "tip_thickness": 56.8827389977048 + }, + "values": [ + 0.16017363965511322, + 86.90072631835938, + -21086.689453125 + ] + }, + { + "number": 322, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.13515426218509674, + 73.69579315185547, + -15871.068359375 + ] + }, + { + "number": 323, + "params": { + "support_angle": 20.911091282577488, + "tip_thickness": 32.83328882267785 + }, + "values": [ + 0.10678848624229431, + 74.6731948852539, + -8407.2421875 + ] + }, + { + "number": 324, + "params": { + "support_angle": 47.031756080505325, + "tip_thickness": 32.56042394981304 + }, + "values": [ + 0.10773077607154846, + 78.16789245605469, + -9068.22265625 + ] + }, + { + "number": 325, + "params": { + "support_angle": 54.150338670817845, + "tip_thickness": 32.135659453806866 + }, + "values": [ + 0.11080149561166763, + 78.32801055908203, + -10234.2373046875 + ] + }, + { + "number": 327, + "params": { + "support_angle": 21.16359678679129, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12026900053024292, + 73.86305236816406, + -11451.171875 + ] + }, + { + "number": 329, + "params": { + "support_angle": 52.49819653888826, + "tip_thickness": 51.059006317731104 + }, + "values": [ + 0.1400517374277115, + 76.39205932617188, + -16919.052734375 + ] + }, + { + "number": 331, + "params": { + "support_angle": 54.836858268207536, + "tip_thickness": 48.86828540339652 + }, + "values": [ + 0.1352478712797165, + 80.35325622558594, + -16156.3125 + ] + }, + { + "number": 332, + "params": { + "support_angle": 61.7651247794619, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.16160590946674347, + 87.83258056640625, + -21390.4609375 + ] + }, + { + "number": 333, + "params": { + "support_angle": 60.372007758203125, + "tip_thickness": 56.32416040581159 + }, + "values": [ + 0.1594405621290207, + 86.34780883789062, + -20923.333984375 + ] + }, + { + "number": 334, + "params": { + "support_angle": 56.75355219019429, + "tip_thickness": 54.10442791154546 + }, + "values": [ + 0.15528073906898499, + 83.74237060546875, + -19961.99609375 + ] + }, + { + "number": 335, + "params": { + "support_angle": 34.101728628565326, + "tip_thickness": 35.32318631339169 + }, + "values": [ + 0.10459721833467484, + 76.76976776123047, + -8026.22265625 + ] + }, + { + "number": 336, + "params": { + "support_angle": 67.7432640331597, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12664642930030823, + 81.0645980834961, + -15036.3544921875 + ] + }, + { + "number": 337, + "params": { + "support_angle": 57.53073758204292, + "tip_thickness": 54.20504217801792 + }, + "values": [ + 0.15564224123954773, + 83.98999786376953, + -20053.927734375 + ] + }, + { + "number": 338, + "params": { + "support_angle": 69.52525710003366, + "tip_thickness": 45.48901044903586 + }, + "values": [ + 0.12972643971443176, + 81.66741180419922, + -15370.5244140625 + ] + }, + { + "number": 344, + "params": { + "support_angle": 67.7432640331597, + "tip_thickness": 57.922719768106944 + }, + "values": [ + 0.16007262468338013, + 86.89249420166016, + -21085.609375 + ] + }, + { + "number": 346, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 42.86982082125055 + }, + "values": [ + 0.12775066494941711, + 81.54261016845703, + -15155.83203125 + ] + }, + { + "number": 348, + "params": { + "support_angle": 57.727143704234116, + "tip_thickness": 33.09371606507798 + }, + "values": [ + 0.11371732503175735, + 77.38166809082031, + -11397.234375 + ] + }, + { + "number": 350, + "params": { + "support_angle": 65.12764533397834, + "tip_thickness": 49.760808948568354 + }, + "values": [ + 0.13504043221473694, + 81.16626739501953, + -16261.9033203125 + ] + }, + { + "number": 351, + "params": { + "support_angle": 45.26261862239286, + "tip_thickness": 54.79372398323225 + }, + "values": [ + 0.15822772681713104, + 90.09639739990234, + -20691.4140625 + ] + }, + { + "number": 354, + "params": { + "support_angle": 20.541882574014917, + "tip_thickness": 31.223254246642917 + }, + "values": [ + 0.10583827644586563, + 75.38594055175781, + -8210.50390625 + ] + }, + { + "number": 355, + "params": { + "support_angle": 47.031756080505325, + "tip_thickness": 32.56042394981304 + }, + "values": [ + 0.10773077607154846, + 78.16789245605469, + -9068.22265625 + ] + }, + { + "number": 356, + "params": { + "support_angle": 65.26909882096318, + "tip_thickness": 32.73860030358401 + }, + "values": [ + 0.11875332146883011, + 75.00489807128906, + -13197.693359375 + ] + }, + { + "number": 357, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 39.579409127712445 + }, + "values": [ + 0.12375471740961075, + 78.52791595458984, + -14481.1875 + ] + }, + { + "number": 358, + "params": { + "support_angle": 67.50309835254025, + "tip_thickness": 58.51821440812668 + }, + "values": [ + 0.16108828783035278, + 87.33094787597656, + -21287.771484375 + ] + }, + { + "number": 359, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 47.203136643698585 + }, + "values": [ + 0.1306481510400772, + 81.51278686523438, + -15415.6875 + ] + }, + { + "number": 361, + "params": { + "support_angle": 56.480308916903205, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.16093726456165314, + 88.78457641601562, + -21212.0234375 + ] + }, + { + "number": 362, + "params": { + "support_angle": 62.556833575842845, + "tip_thickness": 48.95511636509398 + }, + "values": [ + 0.1332300454378128, + 81.16423034667969, + -15855.916015625 + ] + }, + { + "number": 363, + "params": { + "support_angle": 36.160146601037766, + "tip_thickness": 56.844820512829465 + }, + "values": [ + 0.15040145814418793, + 89.85953521728516, + -19014.34375 + ] + }, + { + "number": 364, + "params": { + "support_angle": 42.42227609891599, + "tip_thickness": 38.79632315094194 + }, + "values": [ + 0.1065686047077179, + 77.48643493652344, + -8643.8525390625 + ] + }, + { + "number": 366, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.1520288735628128, + 82.85697174072266, + -19376.578125 + ] + }, + { + "number": 367, + "params": { + "support_angle": 53.62592280385192, + "tip_thickness": 52.5712358831304 + }, + "values": [ + 0.14696282148361206, + 79.26005554199219, + -18307.345703125 + ] + }, + { + "number": 368, + "params": { + "support_angle": 59.578952186292426, + "tip_thickness": 53.68854428383662 + }, + "values": [ + 0.15427649021148682, + 83.4999771118164, + -19809.685546875 + ] + }, + { + "number": 370, + "params": { + "support_angle": 60.372007758203125, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.15250593423843384, + 82.89993286132812, + -19456.6953125 + ] + }, + { + "number": 371, + "params": { + "support_angle": 34.101728628565326, + "tip_thickness": 31.72676280049933 + }, + "values": [ + 0.10412248969078064, + 77.35938262939453, + -7905.71533203125 + ] + }, + { + "number": 373, + "params": { + "support_angle": 42.07652506866885, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.105756975710392, + 77.69410705566406, + -8421.06640625 + ] + }, + { + "number": 374, + "params": { + "support_angle": 28.718321450249572, + "tip_thickness": 37.50728694493786 + }, + "values": [ + 0.10889549553394318, + 73.74740600585938, + -8910.400390625 + ] + }, + { + "number": 375, + "params": { + "support_angle": 64.38520913791498, + "tip_thickness": 40.527450376562356 + }, + "values": [ + 0.1259932965040207, + 80.64866638183594, + -14904.287109375 + ] + }, + { + "number": 376, + "params": { + "support_angle": 25.853350821380293, + "tip_thickness": 34.28975046158507 + }, + "values": [ + 0.10631345957517624, + 75.22620391845703, + -8335.935546875 + ] + }, + { + "number": 379, + "params": { + "support_angle": 61.7651247794619, + "tip_thickness": 33.03368028368371 + }, + "values": [ + 0.11667238175868988, + 76.03097534179688, + -12574.3544921875 + ] + }, + { + "number": 381, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 58.83571691471742 + }, + "values": [ + 0.16174301505088806, + 88.0146255493164, + -21418.71484375 + ] + }, + { + "number": 383, + "params": { + "support_angle": 23.638150318209675, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.10563232004642487, + 75.6432876586914, + -8183.697265625 + ] + }, + { + "number": 384, + "params": { + "support_angle": 49.28877906367316, + "tip_thickness": 32.56042394981304 + }, + "values": [ + 0.10873515903949738, + 78.30964660644531, + -9439.439453125 + ] + }, + { + "number": 385, + "params": { + "support_angle": 27.8218521335543, + "tip_thickness": 37.50728694493786 + }, + "values": [ + 0.10949549078941345, + 73.36516571044922, + -9029.166015625 + ] + }, + { + "number": 386, + "params": { + "support_angle": 20.911091282577488, + "tip_thickness": 32.83328882267785 + }, + "values": [ + 0.10678848624229431, + 74.6731948852539, + -8407.2421875 + ] + }, + { + "number": 387, + "params": { + "support_angle": 61.09300296451781, + "tip_thickness": 51.18726681469489 + }, + "values": [ + 0.14298328757286072, + 81.15311431884766, + -17772.990234375 + ] + }, + { + "number": 388, + "params": { + "support_angle": 24.06743903209499, + "tip_thickness": 32.545131422555755 + }, + "values": [ + 0.10565347969532013, + 75.6430435180664, + -8190.30126953125 + ] + }, + { + "number": 389, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 49.760808948568354 + }, + "values": [ + 0.13568587601184845, + 81.0245361328125, + -16398.44140625 + ] + }, + { + "number": 390, + "params": { + "support_angle": 69.33197892505876, + "tip_thickness": 37.186856720009175 + }, + "values": [ + 0.12330517917871475, + 77.900146484375, + -14474.4609375 + ] + }, + { + "number": 391, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 41.11926441200673 + }, + "values": [ + 0.10984507948160172, + 74.45667266845703, + -9378.255859375 + ] + }, + { + "number": 392, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 35.873734043678894 + }, + "values": [ + 0.12113423645496368, + 75.36341857910156, + -13873.990234375 + ] + }, + { + "number": 394, + "params": { + "support_angle": 67.36242886919294, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12659968435764313, + 81.04085540771484, + -15024.0791015625 + ] + }, + { + "number": 395, + "params": { + "support_angle": 69.52525710003366, + "tip_thickness": 59.58003191468613 + }, + "values": [ + 0.16161087155342102, + 87.62229919433594, + -21404.20703125 + ] + }, + { + "number": 398, + "params": { + "support_angle": 68.18099885446264, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.11986880749464035, + 74.57935333251953, + -13508.63671875 + ] + }, + { + "number": 399, + "params": { + "support_angle": 57.727143704234116, + "tip_thickness": 33.09371606507798 + }, + "values": [ + 0.11371732503175735, + 77.38166809082031, + -11397.234375 + ] + }, + { + "number": 400, + "params": { + "support_angle": 58.85734579637184, + "tip_thickness": 32.56042394981304 + }, + "values": [ + 0.11402740329504013, + 77.21837615966797, + -11519.345703125 + ] + }, + { + "number": 402, + "params": { + "support_angle": 60.372007758203125, + "tip_thickness": 56.32416040581159 + }, + "values": [ + 0.1594405621290207, + 86.34780883789062, + -20923.333984375 + ] + }, + { + "number": 403, + "params": { + "support_angle": 62.556833575842845, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.11639370024204254, + 76.11581420898438, + -12481.2548828125 + ] + }, + { + "number": 404, + "params": { + "support_angle": 65.3177192547368, + "tip_thickness": 33.33592446918454 + }, + "values": [ + 0.11949346214532852, + 74.7572021484375, + -13391.5458984375 + ] + }, + { + "number": 406, + "params": { + "support_angle": 44.63125521454296, + "tip_thickness": 47.724997817070324 + }, + "values": [ + 0.13156290352344513, + 75.05387878417969, + -14950.322265625 + ] + }, + { + "number": 407, + "params": { + "support_angle": 20.567682238370953, + "tip_thickness": 35.32318631339169 + }, + "values": [ + 0.10973043739795685, + 72.85752868652344, + -9028.525390625 + ] + }, + { + "number": 408, + "params": { + "support_angle": 43.43303209970631, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.1557929664850235, + 89.45000457763672, + -20180.455078125 + ] + }, + { + "number": 409, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 56.32416040581159 + }, + "values": [ + 0.15315702557563782, + 90.08006286621094, + -19635.3203125 + ] + }, + { + "number": 411, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 58.83571691471742 + }, + "values": [ + 0.16174301505088806, + 88.0146255493164, + -21418.71484375 + ] + }, + { + "number": 412, + "params": { + "support_angle": 61.09300296451781, + "tip_thickness": 51.18726681469489 + }, + "values": [ + 0.14298328757286072, + 81.15311431884766, + -17772.990234375 + ] + }, + { + "number": 413, + "params": { + "support_angle": 39.65488623333802, + "tip_thickness": 45.48901044903586 + }, + "values": [ + 0.12851136922836304, + 71.92572021484375, + -13918.9033203125 + ] + }, + { + "number": 414, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 33.564537488042156 + }, + "values": [ + 0.11705794185400009, + 75.85871124267578, + -12697.833984375 + ] + }, + { + "number": 415, + "params": { + "support_angle": 25.876312338855243, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11840229481458664, + 71.31527709960938, + -11145.3955078125 + ] + }, + { + "number": 416, + "params": { + "support_angle": 47.13223173537883, + "tip_thickness": 49.47630906348191 + }, + "values": [ + 0.1331189125776291, + 73.75684356689453, + -15419.94140625 + ] + }, + { + "number": 418, + "params": { + "support_angle": 61.7651247794619, + "tip_thickness": 47.50106295291479 + }, + "values": [ + 0.13084593415260315, + 81.38060760498047, + -15385.744140625 + ] + }, + { + "number": 422, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 56.844820512829465 + }, + "values": [ + 0.16003653407096863, + 86.71588134765625, + -21072.396484375 + ] + }, + { + "number": 423, + "params": { + "support_angle": 49.28877906367316, + "tip_thickness": 58.89667618321834 + }, + "values": [ + 0.16070213913917542, + 90.74663543701172, + -21163.388671875 + ] + }, + { + "number": 425, + "params": { + "support_angle": 64.38520913791498, + "tip_thickness": 40.527450376562356 + }, + "values": [ + 0.1259932965040207, + 80.64866638183594, + -14904.287109375 + ] + }, + { + "number": 426, + "params": { + "support_angle": 68.49394133538195, + "tip_thickness": 54.20504217801792 + }, + "values": [ + 0.15094968676567078, + 83.39273834228516, + -19172.044921875 + ] + }, + { + "number": 427, + "params": { + "support_angle": 69.52525710003366, + "tip_thickness": 45.48901044903586 + }, + "values": [ + 0.12972643971443176, + 81.66741180419922, + -15370.5244140625 + ] + }, + { + "number": 428, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 37.186856720009175 + }, + "values": [ + 0.12223504483699799, + 76.6735610961914, + -14162.1201171875 + ] + }, + { + "number": 429, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 53.68854428383662 + }, + "values": [ + 0.15350614488124847, + 83.44685363769531, + -19657.953125 + ] + }, + { + "number": 432, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 37.186856720009175 + }, + "values": [ + 0.12223504483699799, + 76.6735610961914, + -14162.1201171875 + ] + }, + { + "number": 434, + "params": { + "support_angle": 69.64823980596502, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.1268896460533142, + 81.18171691894531, + -15092.94921875 + ] + }, + { + "number": 435, + "params": { + "support_angle": 68.18099885446264, + "tip_thickness": 32.213896942061965 + }, + "values": [ + 0.1197647750377655, + 74.6049575805664, + -13481.185546875 + ] + }, + { + "number": 436, + "params": { + "support_angle": 62.556833575842845, + "tip_thickness": 53.948855369003255 + }, + "values": [ + 0.15378427505493164, + 83.72016143798828, + -19733.775390625 + ] + }, + { + "number": 438, + "params": { + "support_angle": 69.52525710003366, + "tip_thickness": 45.692935325104465 + }, + "values": [ + 0.12982815504074097, + 81.65967559814453, + -15374.9423828125 + ] + }, + { + "number": 439, + "params": { + "support_angle": 34.101728628565326, + "tip_thickness": 35.32318631339169 + }, + "values": [ + 0.10459721833467484, + 76.76976776123047, + -8026.22265625 + ] + }, + { + "number": 444, + "params": { + "support_angle": 65.05790052454944, + "tip_thickness": 54.79372398323225 + }, + "values": [ + 0.1547449678182602, + 84.45211029052734, + -19966.693359375 + ] + }, + { + "number": 445, + "params": { + "support_angle": 36.160146601037766, + "tip_thickness": 45.563718652300985 + }, + "values": [ + 0.1281682699918747, + 72.67020416259766, + -13502.1982421875 + ] + }, + { + "number": 446, + "params": { + "support_angle": 53.62592280385192, + "tip_thickness": 31.363391410243736 + }, + "values": [ + 0.11017364263534546, + 78.35211944580078, + -9989.2060546875 + ] + }, + { + "number": 447, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 37.186856720009175 + }, + "values": [ + 0.12223504483699799, + 76.6735610961914, + -14162.1201171875 + ] + }, + { + "number": 448, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 51.0290739437736 + }, + "values": [ + 0.14080113172531128, + 81.17603302001953, + -17426.267578125 + ] + }, + { + "number": 454, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 55.47670231482534 + }, + "values": [ + 0.1562797874212265, + 85.17513275146484, + -20285.65234375 + ] + }, + { + "number": 457, + "params": { + "support_angle": 68.47923138822793, + "tip_thickness": 35.64363479171284 + }, + "values": [ + 0.12175045162439346, + 76.03255462646484, + -14063.90625 + ] + }, + { + "number": 459, + "params": { + "support_angle": 37.667611401302636, + "tip_thickness": 38.42889568776691 + }, + "values": [ + 0.10547399520874023, + 76.5738754272461, + -8258.646484375 + ] + }, + { + "number": 460, + "params": { + "support_angle": 47.031756080505325, + "tip_thickness": 33.564537488042156 + }, + "values": [ + 0.10795918107032776, + 78.17778015136719, + -9145.5341796875 + ] + }, + { + "number": 461, + "params": { + "support_angle": 57.66890926294708, + "tip_thickness": 47.509683355526164 + }, + "values": [ + 0.13077013194561005, + 81.2120132446289, + -15285.0634765625 + ] + }, + { + "number": 463, + "params": { + "support_angle": 69.31053722398013, + "tip_thickness": 38.42889568776691 + }, + "values": [ + 0.12485501915216446, + 79.44828796386719, + -14766.6728515625 + ] + }, + { + "number": 464, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 37.186856720009175 + }, + "values": [ + 0.12223504483699799, + 76.6735610961914, + -14162.1201171875 + ] + }, + { + "number": 467, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 51.87021504122962 + }, + "values": [ + 0.14298146963119507, + 81.5997085571289, + -17783.71484375 + ] + }, + { + "number": 468, + "params": { + "support_angle": 60.68975098534743, + "tip_thickness": 50.54193517661638 + }, + "values": [ + 0.13996535539627075, + 80.8857192993164, + -17265.53125 + ] + }, + { + "number": 469, + "params": { + "support_angle": 54.80148983374865, + "tip_thickness": 34.87850818034674 + }, + "values": [ + 0.1132570207118988, + 77.68869018554688, + -11198.9404296875 + ] + }, + { + "number": 470, + "params": { + "support_angle": 65.54635922469213, + "tip_thickness": 54.67611728769507 + }, + "values": [ + 0.154187873005867, + 84.25480651855469, + -19850.08203125 + ] + }, + { + "number": 472, + "params": { + "support_angle": 67.4899956645962, + "tip_thickness": 49.760808948568354 + }, + "values": [ + 0.13457240164279938, + 81.29335021972656, + -16149.1767578125 + ] + }, + { + "number": 473, + "params": { + "support_angle": 56.285975419418, + "tip_thickness": 49.364168877215036 + }, + "values": [ + 0.13702037930488586, + 80.4298324584961, + -16589.41796875 + ] + }, + { + "number": 475, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 33.09371606507798 + }, + "values": [ + 0.11993840336799622, + 74.60623168945312, + -13516.6298828125 + ] + }, + { + "number": 476, + "params": { + "support_angle": 63.30319447502042, + "tip_thickness": 31.356560103185682 + }, + "values": [ + 0.11584639549255371, + 76.31604766845703, + -12280.6611328125 + ] + }, + { + "number": 477, + "params": { + "support_angle": 47.031756080505325, + "tip_thickness": 30.79100923491756 + }, + "values": [ + 0.10741978138685226, + 78.14544677734375, + -8969.2265625 + ] + }, + { + "number": 484, + "params": { + "support_angle": 68.49571989073016, + "tip_thickness": 55.263567694071256 + }, + "values": [ + 0.1540515124797821, + 84.48320007324219, + -19833.76171875 + ] + }, + { + "number": 485, + "params": { + "support_angle": 61.91643523555689, + "tip_thickness": 31.363391410243736 + }, + "values": [ + 0.1149846762418747, + 76.74849700927734, + -11910.05859375 + ] + }, + { + "number": 486, + "params": { + "support_angle": 38.779147631997205, + "tip_thickness": 43.01554947713919 + }, + "values": [ + 0.1213734969496727, + 71.38740539550781, + -11917.83203125 + ] + }, + { + "number": 487, + "params": { + "support_angle": 43.434657989748516, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.15579427778720856, + 89.44987487792969, + -20180.71484375 + ] + }, + { + "number": 488, + "params": { + "support_angle": 40.74097511688326, + "tip_thickness": 52.5712358831304 + }, + "values": [ + 0.1515609323978424, + 88.9493408203125, + -19244.7734375 + ] + }, + { + "number": 489, + "params": { + "support_angle": 21.16359678679129, + "tip_thickness": 38.20221215792119 + }, + "values": [ + 0.11460716277360916, + 71.08891296386719, + -10131.296875 + ] + }, + { + "number": 491, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.1434634029865265, + 81.77892303466797, + -17862.9765625 + ] + }, + { + "number": 493, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 37.186856720009175 + }, + "values": [ + 0.12223504483699799, + 76.6735610961914, + -14162.1201171875 + ] + }, + { + "number": 494, + "params": { + "support_angle": 63.23611881275266, + "tip_thickness": 54.387030273902326 + }, + "values": [ + 0.15468855202198029, + 84.21249389648438, + -19939.05078125 + ] + }, + { + "number": 495, + "params": { + "support_angle": 51.87149507491033, + "tip_thickness": 51.78274001167985 + }, + "values": [ + 0.1397484540939331, + 75.62481689453125, + -16853.125 + ] + }, + { + "number": 496, + "params": { + "support_angle": 69.98588366430653, + "tip_thickness": 59.89910511221716 + }, + "values": [ + 0.16177749633789062, + 87.70994567871094, + -21440.455078125 + ] + }, + { + "number": 498, + "params": { + "support_angle": 61.7651247794619, + "tip_thickness": 58.34297189647284 + }, + "values": [ + 0.16151058673858643, + 87.75012969970703, + -21370.521484375 + ] + }, + { + "number": 499, + "params": { + "support_angle": 62.482369533870575, + "tip_thickness": 37.42044305229593 + }, + "values": [ + 0.121547631919384, + 75.90522003173828, + -13930.1728515625 + ] + }, + { + "number": 502, + "params": { + "support_angle": 50.308731722544, + "tip_thickness": 51.78274001167985 + }, + "values": [ + 0.13808685541152954, + 75.2091064453125, + -16485.4140625 + ] + }, + { + "number": 505, + "params": { + "support_angle": 53.58503422029283, + "tip_thickness": 48.95511636509398 + }, + "values": [ + 0.13621997833251953, + 80.0734634399414, + -16311.234375 + ] + }, + { + "number": 507, + "params": { + "support_angle": 37.90813590164203, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.10465079545974731, + 77.53746795654297, + -8068.45263671875 + ] + }, + { + "number": 508, + "params": { + "support_angle": 68.18099885446264, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.11986880749464035, + 74.57935333251953, + -13508.63671875 + ] + }, + { + "number": 509, + "params": { + "support_angle": 25.677879609981453, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.10510814934968948, + 76.0804672241211, + -8094.35595703125 + ] + }, + { + "number": 511, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 53.16955175206918 + }, + "values": [ + 0.15092425048351288, + 82.725341796875, + -19165.873046875 + ] + }, + { + "number": 513, + "params": { + "support_angle": 46.008175055599665, + "tip_thickness": 49.760808948568354 + }, + "values": [ + 0.13555264472961426, + 75.01834106445312, + -15978.81640625 + ] + }, + { + "number": 514, + "params": { + "support_angle": 36.160146601037766, + "tip_thickness": 56.844820512829465 + }, + "values": [ + 0.15040145814418793, + 89.85953521728516, + -19014.34375 + ] + }, + { + "number": 517, + "params": { + "support_angle": 60.372007758203125, + "tip_thickness": 47.529429357693004 + }, + "values": [ + 0.13083454966545105, + 81.32588195800781, + -15355.90234375 + ] + }, + { + "number": 518, + "params": { + "support_angle": 64.51318919454582, + "tip_thickness": 56.29960807975035 + }, + "values": [ + 0.1586838662624359, + 86.11131286621094, + -20794.4453125 + ] + }, + { + "number": 520, + "params": { + "support_angle": 40.174143310619854, + "tip_thickness": 34.02045685351922 + }, + "values": [ + 0.10517136752605438, + 77.5428695678711, + -8224.330078125 + ] + }, + { + "number": 521, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 30.86348028940017 + }, + "values": [ + 0.11419215053319931, + 77.0737533569336, + -11596.681640625 + ] + }, + { + "number": 522, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 52.65411767020857 + }, + "values": [ + 0.14589856564998627, + 82.0452651977539, + -18246.572265625 + ] + }, + { + "number": 523, + "params": { + "support_angle": 53.62592280385192, + "tip_thickness": 49.760808948568354 + }, + "values": [ + 0.1399957686662674, + 79.36088562011719, + -17080.6328125 + ] + }, + { + "number": 524, + "params": { + "support_angle": 51.01547756767323, + "tip_thickness": 51.12239304297671 + }, + "values": [ + 0.13670095801353455, + 74.41380310058594, + -16192.8583984375 + ] + }, + { + "number": 526, + "params": { + "support_angle": 35.58555380447055, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11189951002597809, + 72.98871612548828, + -9688.0703125 + ] + }, + { + "number": 527, + "params": { + "support_angle": 30.648208075445538, + "tip_thickness": 41.11926441200673 + }, + "values": [ + 0.11765807867050171, + 70.9090576171875, + -10970.74609375 + ] + }, + { + "number": 529, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 59.89910511221716 + }, + "values": [ + 0.16226249933242798, + 88.33602142333984, + -21518.818359375 + ] + }, + { + "number": 530, + "params": { + "support_angle": 60.372007758203125, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.15250593423843384, + 82.89993286132812, + -19456.6953125 + ] + }, + { + "number": 532, + "params": { + "support_angle": 53.62592280385192, + "tip_thickness": 52.5712358831304 + }, + "values": [ + 0.14696282148361206, + 79.26005554199219, + -18307.345703125 + ] + }, + { + "number": 536, + "params": { + "support_angle": 40.74097511688326, + "tip_thickness": 41.76732135299197 + }, + "values": [ + 0.11027640849351883, + 75.18965148925781, + -9581.4755859375 + ] + }, + { + "number": 537, + "params": { + "support_angle": 63.23611881275266, + "tip_thickness": 54.387030273902326 + }, + "values": [ + 0.15468855202198029, + 84.21249389648438, + -19939.05078125 + ] + }, + { + "number": 538, + "params": { + "support_angle": 41.873746101186455, + "tip_thickness": 57.12476083481245 + }, + "values": [ + 0.1565779149532318, + 90.56619262695312, + -20374.392578125 + ] + }, + { + "number": 539, + "params": { + "support_angle": 37.412773351165015, + "tip_thickness": 47.509683355526164 + }, + "values": [ + 0.1323944330215454, + 72.59355926513672, + -14838.7685546875 + ] + }, + { + "number": 540, + "params": { + "support_angle": 45.69947445799054, + "tip_thickness": 53.50959038223429 + }, + "values": [ + 0.15782234072685242, + 89.56884002685547, + -20596.58203125 + ] + }, + { + "number": 541, + "params": { + "support_angle": 39.827139116063506, + "tip_thickness": 48.662601006836205 + }, + "values": [ + 0.1324741095304489, + 77.41622161865234, + -15345.0869140625 + ] + }, + { + "number": 542, + "params": { + "support_angle": 63.11818543733726, + "tip_thickness": 50.54193517661638 + }, + "values": [ + 0.13854295015335083, + 81.04024505615234, + -17018.9375 + ] + }, + { + "number": 543, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 58.485618709729266 + }, + "values": [ + 0.16157421469688416, + 87.84851837158203, + -21385.03125 + ] + }, + { + "number": 544, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 34.41220442787114 + }, + "values": [ + 0.12072443217039108, + 74.81209564208984, + -13772.7333984375 + ] + }, + { + "number": 545, + "params": { + "support_angle": 34.101728628565326, + "tip_thickness": 57.79762875484484 + }, + "values": [ + 0.14903263747692108, + 89.78660583496094, + -18700.154296875 + ] + }, + { + "number": 547, + "params": { + "support_angle": 44.60581465397691, + "tip_thickness": 37.74733164896875 + }, + "values": [ + 0.10790778696537018, + 77.84158325195312, + -9087.091796875 + ] + }, + { + "number": 548, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 43.774072687147836 + }, + "values": [ + 0.1237463727593422, + 71.79258728027344, + -12453.8076171875 + ] + }, + { + "number": 549, + "params": { + "support_angle": 49.28877906367316, + "tip_thickness": 51.87021504122962 + }, + "values": [ + 0.14244265854358673, + 78.02801513671875, + -17364.94140625 + ] + }, + { + "number": 550, + "params": { + "support_angle": 69.00162876427385, + "tip_thickness": 44.77854281978609 + }, + "values": [ + 0.12913601100444794, + 81.67733764648438, + -15304.298828125 + ] + }, + { + "number": 551, + "params": { + "support_angle": 36.43758051437541, + "tip_thickness": 32.70869310163225 + }, + "values": [ + 0.10437238961458206, + 77.46517944335938, + -7989.04345703125 + ] + }, + { + "number": 552, + "params": { + "support_angle": 51.67004271583629, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.1416398137807846, + 76.74089813232422, + -17238.345703125 + ] + }, + { + "number": 554, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 30.86348028940017 + }, + "values": [ + 0.11419215053319931, + 77.0737533569336, + -11596.681640625 + ] + }, + { + "number": 555, + "params": { + "support_angle": 23.793166405433197, + "tip_thickness": 33.866391657319475 + }, + "values": [ + 0.1066693440079689, + 74.87189483642578, + -8396.8515625 + ] + }, + { + "number": 556, + "params": { + "support_angle": 26.40229194788862, + "tip_thickness": 34.55708080536883 + }, + "values": [ + 0.10633860528469086, + 75.2339096069336, + -8344.9404296875 + ] + }, + { + "number": 557, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 43.774072687147836 + }, + "values": [ + 0.1237463727593422, + 71.79258728027344, + -12453.8076171875 + ] + }, + { + "number": 559, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 51.0290739437736 + }, + "values": [ + 0.14080113172531128, + 81.17603302001953, + -17426.267578125 + ] + }, + { + "number": 560, + "params": { + "support_angle": 52.04373724016073, + "tip_thickness": 35.45640253197435 + }, + "values": [ + 0.11170162260532379, + 78.23872375488281, + -10557.056640625 + ] + }, + { + "number": 561, + "params": { + "support_angle": 37.28336416619316, + "tip_thickness": 57.12476083481245 + }, + "values": [ + 0.15209011733531952, + 90.07546997070312, + -19402.255859375 + ] + }, + { + "number": 562, + "params": { + "support_angle": 40.74097511688326, + "tip_thickness": 56.29960807975035 + }, + "values": [ + 0.15544861555099487, + 90.36946868896484, + -20137.48828125 + ] + }, + { + "number": 564, + "params": { + "support_angle": 53.37788692605136, + "tip_thickness": 35.169596136048895 + }, + "values": [ + 0.11245755106210709, + 78.02384185791016, + -10873.4375 + ] + }, + { + "number": 567, + "params": { + "support_angle": 22.043430813323944, + "tip_thickness": 35.06805189216494 + }, + "values": [ + 0.10867939889431, + 73.548095703125, + -8804.1142578125 + ] + }, + { + "number": 568, + "params": { + "support_angle": 33.92951695159793, + "tip_thickness": 35.310314528302406 + }, + "values": [ + 0.10462487488985062, + 76.73978424072266, + -8033.11181640625 + ] + }, + { + "number": 569, + "params": { + "support_angle": 24.43512668785278, + "tip_thickness": 33.61907613301803 + }, + "values": [ + 0.10626896470785141, + 75.19715881347656, + -8317.7236328125 + ] + }, + { + "number": 571, + "params": { + "support_angle": 60.372007758203125, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.15250593423843384, + 82.89993286132812, + -19456.6953125 + ] + }, + { + "number": 572, + "params": { + "support_angle": 30.316685920289625, + "tip_thickness": 40.92809583144226 + }, + "values": [ + 0.11720094084739685, + 70.90044403076172, + -10845.083984375 + ] + }, + { + "number": 573, + "params": { + "support_angle": 45.17086354274284, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11090771108865738, + 77.80220031738281, + -10136.8212890625 + ] + }, + { + "number": 577, + "params": { + "support_angle": 49.544647159412094, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.1454390287399292, + 79.58540344238281, + -17924.70703125 + ] + }, + { + "number": 579, + "params": { + "support_angle": 62.556833575842845, + "tip_thickness": 32.45277095846616 + }, + "values": [ + 0.11654253304004669, + 76.05097961425781, + -12535.8330078125 + ] + }, + { + "number": 581, + "params": { + "support_angle": 66.04362002659066, + "tip_thickness": 31.832338795645914 + }, + "values": [ + 0.11812083423137665, + 75.21687316894531, + -13035.3427734375 + ] + }, + { + "number": 582, + "params": { + "support_angle": 33.843882407360184, + "tip_thickness": 38.42889568776691 + }, + "values": [ + 0.10723932087421417, + 75.1327133178711, + -8625.634765625 + ] + }, + { + "number": 583, + "params": { + "support_angle": 60.31006398965307, + "tip_thickness": 33.564537488042156 + }, + "values": [ + 0.11628593504428864, + 76.26974487304688, + -12411.9462890625 + ] + }, + { + "number": 586, + "params": { + "support_angle": 30.467466166835514, + "tip_thickness": 38.42889568776691 + }, + "values": [ + 0.1096307560801506, + 73.48181915283203, + -9092.99609375 + ] + }, + { + "number": 588, + "params": { + "support_angle": 40.74097511688326, + "tip_thickness": 41.76732135299197 + }, + "values": [ + 0.11027640849351883, + 75.18965148925781, + -9581.4755859375 + ] + }, + { + "number": 589, + "params": { + "support_angle": 44.226149259551065, + "tip_thickness": 48.54764314590888 + }, + "values": [ + 0.1340673565864563, + 75.24632263183594, + -15672.560546875 + ] + }, + { + "number": 590, + "params": { + "support_angle": 38.44568197848862, + "tip_thickness": 43.876041483994435 + }, + "values": [ + 0.1241162046790123, + 72.0325698852539, + -12538.04296875 + ] + }, + { + "number": 591, + "params": { + "support_angle": 56.285975419418, + "tip_thickness": 49.364168877215036 + }, + "values": [ + 0.13702037930488586, + 80.4298324584961, + -16589.41796875 + ] + }, + { + "number": 592, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 33.564537488042156 + }, + "values": [ + 0.12036041915416718, + 74.60269165039062, + -13649.1787109375 + ] + }, + { + "number": 593, + "params": { + "support_angle": 57.37354690668783, + "tip_thickness": 31.100496086717936 + }, + "values": [ + 0.11184666305780411, + 78.0647964477539, + -10669.05078125 + ] + }, + { + "number": 595, + "params": { + "support_angle": 47.031756080505325, + "tip_thickness": 37.57310833032062 + }, + "values": [ + 0.10968606919050217, + 78.15840148925781, + -9751.7314453125 + ] + }, + { + "number": 598, + "params": { + "support_angle": 68.18099885446264, + "tip_thickness": 33.21516034019328 + }, + "values": [ + 0.12042336165904999, + 74.605712890625, + -13677.8623046875 + ] + }, + { + "number": 600, + "params": { + "support_angle": 46.63086332275115, + "tip_thickness": 37.27411510904189 + }, + "values": [ + 0.1091788038611412, + 78.11178588867188, + -9565.4375 + ] + }, + { + "number": 601, + "params": { + "support_angle": 53.58503422029283, + "tip_thickness": 48.95511636509398 + }, + "values": [ + 0.13621997833251953, + 80.0734634399414, + -16311.234375 + ] + }, + { + "number": 602, + "params": { + "support_angle": 33.46216154746905, + "tip_thickness": 41.31852489313868 + }, + "values": [ + 0.11688277870416641, + 71.09242248535156, + -10769.4072265625 + ] + }, + { + "number": 603, + "params": { + "support_angle": 49.544647159412094, + "tip_thickness": 30.60213593333179 + }, + "values": [ + 0.10831475257873535, + 78.31732177734375, + -9290.1015625 + ] + }, + { + "number": 604, + "params": { + "support_angle": 67.4899956645962, + "tip_thickness": 49.760808948568354 + }, + "values": [ + 0.13457240164279938, + 81.29335021972656, + -16149.1767578125 + ] + }, + { + "number": 605, + "params": { + "support_angle": 36.10395827915892, + "tip_thickness": 36.34344020989634 + }, + "values": [ + 0.10465418547391891, + 76.90731048583984, + -8044.5703125 + ] + }, + { + "number": 606, + "params": { + "support_angle": 36.374867608895734, + "tip_thickness": 33.59286395457754 + }, + "values": [ + 0.10435855388641357, + 77.39923095703125, + -7976.91455078125 + ] + }, + { + "number": 607, + "params": { + "support_angle": 64.52636403699475, + "tip_thickness": 47.80777360662146 + }, + "values": [ + 0.13123591244220734, + 81.43377685546875, + -15485.8095703125 + ] + }, + { + "number": 608, + "params": { + "support_angle": 53.95511595722448, + "tip_thickness": 53.67513715822015 + }, + "values": [ + 0.1523524671792984, + 82.17040252685547, + -19258.921875 + ] + }, + { + "number": 609, + "params": { + "support_angle": 44.92210994645286, + "tip_thickness": 54.20504217801792 + }, + "values": [ + 0.15765875577926636, + 89.89452362060547, + -20579.876953125 + ] + }, + { + "number": 611, + "params": { + "support_angle": 45.17086354274284, + "tip_thickness": 47.509683355526164 + }, + "values": [ + 0.13086751103401184, + 75.36363983154297, + -14838.3076171875 + ] + }, + { + "number": 614, + "params": { + "support_angle": 22.043430813323944, + "tip_thickness": 33.82740908386691 + }, + "values": [ + 0.10727797448635101, + 74.39733123779297, + -8514.63671875 + ] + }, + { + "number": 615, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 38.51327717396173 + }, + "values": [ + 0.1242872104048729, + 78.90756225585938, + -14646.033203125 + ] + }, + { + "number": 618, + "params": { + "support_angle": 69.32576243964898, + "tip_thickness": 48.173244580706616 + }, + "values": [ + 0.13159671425819397, + 81.5106430053711, + -15595.0791015625 + ] + }, + { + "number": 619, + "params": { + "support_angle": 30.316685920289625, + "tip_thickness": 37.11680375207983 + }, + "values": [ + 0.1073324978351593, + 74.78288269042969, + -8589.4697265625 + ] + }, + { + "number": 620, + "params": { + "support_angle": 25.089123631020186, + "tip_thickness": 34.58577417552996 + }, + "values": [ + 0.10682570934295654, + 74.81366729736328, + -8437.08203125 + ] + }, + { + "number": 621, + "params": { + "support_angle": 32.29788641922541, + "tip_thickness": 34.82044119778667 + }, + "values": [ + 0.10475262254476547, + 76.55731964111328, + -8055.2822265625 + ] + }, + { + "number": 623, + "params": { + "support_angle": 34.254758434692356, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.10414350777864456, + 77.3224868774414, + -7909.28466796875 + ] + }, + { + "number": 624, + "params": { + "support_angle": 28.66867976473774, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11732129007577896, + 70.90882110595703, + -10882.4091796875 + ] + }, + { + "number": 625, + "params": { + "support_angle": 64.83827123132126, + "tip_thickness": 33.564537488042156 + }, + "values": [ + 0.11949087679386139, + 74.77289581298828, + -13387.26171875 + ] + }, + { + "number": 626, + "params": { + "support_angle": 47.031756080505325, + "tip_thickness": 31.832338795645914 + }, + "values": [ + 0.10759051144123077, + 78.16117858886719, + -9021.48046875 + ] + }, + { + "number": 628, + "params": { + "support_angle": 40.74097511688326, + "tip_thickness": 52.5712358831304 + }, + "values": [ + 0.1515609323978424, + 88.9493408203125, + -19244.7734375 + ] + }, + { + "number": 629, + "params": { + "support_angle": 66.04362002659066, + "tip_thickness": 55.47670231482534 + }, + "values": [ + 0.15590615570545197, + 85.07257080078125, + -20210.8828125 + ] + }, + { + "number": 630, + "params": { + "support_angle": 40.51984134948307, + "tip_thickness": 56.29960807975035 + }, + "values": [ + 0.15523649752140045, + 90.33307647705078, + -20089.5625 + ] + }, + { + "number": 633, + "params": { + "support_angle": 68.47352166376845, + "tip_thickness": 43.774072687147836 + }, + "values": [ + 0.12840253114700317, + 81.64420318603516, + -15240.6005859375 + ] + }, + { + "number": 634, + "params": { + "support_angle": 63.275356294699016, + "tip_thickness": 54.512162128478394 + }, + "values": [ + 0.15501917898654938, + 84.37202453613281, + -20010.48046875 + ] + }, + { + "number": 635, + "params": { + "support_angle": 49.544647159412094, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.1454390287399292, + 79.58540344238281, + -17924.70703125 + ] + }, + { + "number": 636, + "params": { + "support_angle": 32.895141352246995, + "tip_thickness": 35.12662762170197 + }, + "values": [ + 0.10475200414657593, + 76.58741760253906, + -8059.9482421875 + ] + }, + { + "number": 638, + "params": { + "support_angle": 47.83814465069649, + "tip_thickness": 47.14838068409699 + }, + "values": [ + 0.1299915611743927, + 78.21724700927734, + -14485.455078125 + ] + }, + { + "number": 639, + "params": { + "support_angle": 33.998954683014205, + "tip_thickness": 46.82813914606159 + }, + "values": [ + 0.13040867447853088, + 72.59244537353516, + -14117.73046875 + ] + }, + { + "number": 640, + "params": { + "support_angle": 64.8394204953006, + "tip_thickness": 53.084787995758106 + }, + "values": [ + 0.14928719401359558, + 82.51139068603516, + -18853.4765625 + ] + }, + { + "number": 643, + "params": { + "support_angle": 41.271821930820835, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.15339842438697815, + 89.41963195800781, + -19676.31640625 + ] + }, + { + "number": 645, + "params": { + "support_angle": 32.12048662075401, + "tip_thickness": 33.445104742176106 + }, + "values": [ + 0.10428560525178909, + 76.863037109375, + -7944.43115234375 + ] + }, + { + "number": 649, + "params": { + "support_angle": 46.62947162757929, + "tip_thickness": 31.554706104672807 + }, + "values": [ + 0.10739466547966003, + 78.12065124511719, + -8955.9921875 + ] + }, + { + "number": 653, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 39.670615349241686 + }, + "values": [ + 0.12562310695648193, + 80.24555969238281, + -14873.16796875 + ] + }, + { + "number": 655, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 37.63921964291291 + }, + "values": [ + 0.10493292659521103, + 77.17498016357422, + -8118.99462890625 + ] + }, + { + "number": 656, + "params": { + "support_angle": 54.075136111196464, + "tip_thickness": 52.80683579669059 + }, + "values": [ + 0.14879412949085236, + 80.19513702392578, + -18639.35546875 + ] + }, + { + "number": 657, + "params": { + "support_angle": 49.781937030392214, + "tip_thickness": 51.78274001167985 + }, + "values": [ + 0.139591783285141, + 76.2834243774414, + -16814.541015625 + ] + }, + { + "number": 658, + "params": { + "support_angle": 43.57880942750792, + "tip_thickness": 42.355227424418054 + }, + "values": [ + 0.11235575377941132, + 77.0185317993164, + -10321.1796875 + ] + }, + { + "number": 659, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12650717794895172, + 80.9926986694336, + -15000.671875 + ] + }, + { + "number": 660, + "params": { + "support_angle": 37.44341332714977, + "tip_thickness": 57.88587432743478 + }, + "values": [ + 0.15292373299598694, + 90.2458267211914, + -19588.703125 + ] + }, + { + "number": 662, + "params": { + "support_angle": 51.87149507491033, + "tip_thickness": 51.78274001167985 + }, + "values": [ + 0.1397484540939331, + 75.62481689453125, + -16853.125 + ] + }, + { + "number": 665, + "params": { + "support_angle": 66.91702284105189, + "tip_thickness": 35.43699198496981 + }, + "values": [ + 0.12120580673217773, + 75.42221069335938, + -13908.6748046875 + ] + }, + { + "number": 667, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 51.78274001167985 + }, + "values": [ + 0.14556662738323212, + 81.38818359375, + -18170.1015625 + ] + }, + { + "number": 668, + "params": { + "support_angle": 57.056032464502955, + "tip_thickness": 47.234193395397355 + }, + "values": [ + 0.13041797280311584, + 81.239013671875, + -15219.1142578125 + ] + }, + { + "number": 670, + "params": { + "support_angle": 68.49394133538195, + "tip_thickness": 47.43714664267837 + }, + "values": [ + 0.13093677163124084, + 81.55262756347656, + -15492.607421875 + ] + }, + { + "number": 671, + "params": { + "support_angle": 47.69271422006604, + "tip_thickness": 53.858019355796706 + }, + "values": [ + 0.15820857882499695, + 89.03509521484375, + -20638.9140625 + ] + }, + { + "number": 672, + "params": { + "support_angle": 30.081366002387227, + "tip_thickness": 30.86348028940017 + }, + "values": [ + 0.10399133712053299, + 77.0178451538086, + -7873.50048828125 + ] + }, + { + "number": 673, + "params": { + "support_angle": 57.271973709216496, + "tip_thickness": 34.90967828597113 + }, + "values": [ + 0.11554417014122009, + 76.72625732421875, + -12088.01953125 + ] + }, + { + "number": 675, + "params": { + "support_angle": 47.69271422006604, + "tip_thickness": 49.955916620886 + }, + "values": [ + 0.13380876183509827, + 73.63969421386719, + -15561.640625 + ] + }, + { + "number": 677, + "params": { + "support_angle": 64.8394204953006, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.14518560469150543, + 81.76560974121094, + -18136.771484375 + ] + }, + { + "number": 678, + "params": { + "support_angle": 37.941524206175124, + "tip_thickness": 41.93566797137225 + }, + "values": [ + 0.11561990529298782, + 72.031982421875, + -10560.0224609375 + ] + }, + { + "number": 682, + "params": { + "support_angle": 43.13398928348032, + "tip_thickness": 39.041336224924265 + }, + "values": [ + 0.10718335956335068, + 77.54872131347656, + -8837.5390625 + ] + }, + { + "number": 683, + "params": { + "support_angle": 57.380469008812554, + "tip_thickness": 57.79762875484484 + }, + "values": [ + 0.16079950332641602, + 88.06159973144531, + -21182.765625 + ] + }, + { + "number": 684, + "params": { + "support_angle": 45.13601950462396, + "tip_thickness": 36.96638085440452 + }, + "values": [ + 0.10797108709812164, + 77.9444580078125, + -9120.49609375 + ] + }, + { + "number": 685, + "params": { + "support_angle": 64.97872866372842, + "tip_thickness": 41.516736641196346 + }, + "values": [ + 0.12688803672790527, + 81.3020248413086, + -15051.01171875 + ] + }, + { + "number": 686, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.1434634029865265, + 81.77892303466797, + -17862.9765625 + ] + }, + { + "number": 689, + "params": { + "support_angle": 65.32360554822735, + "tip_thickness": 54.20504217801792 + }, + "values": [ + 0.1530117392539978, + 83.74835205078125, + -19595.23828125 + ] + }, + { + "number": 691, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 33.50694122125092 + }, + "values": [ + 0.11699185520410538, + 75.89289093017578, + -12676.61328125 + ] + }, + { + "number": 692, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 55.263567694071256 + }, + "values": [ + 0.15487073361873627, + 84.6832275390625, + -19998.556640625 + ] + }, + { + "number": 693, + "params": { + "support_angle": 66.99160618067376, + "tip_thickness": 48.83124159214253 + }, + "values": [ + 0.13254857063293457, + 81.40390014648438, + -15712.560546875 + ] + }, + { + "number": 694, + "params": { + "support_angle": 64.83827123132126, + "tip_thickness": 33.564537488042156 + }, + "values": [ + 0.11949087679386139, + 74.77289581298828, + -13387.26171875 + ] + }, + { + "number": 695, + "params": { + "support_angle": 36.745280732854305, + "tip_thickness": 59.89910511221716 + }, + "values": [ + 0.1535627245903015, + 90.47978210449219, + -19731.75 + ] + }, + { + "number": 696, + "params": { + "support_angle": 68.49394133538195, + "tip_thickness": 34.17816217990162 + }, + "values": [ + 0.12085454165935516, + 74.96379089355469, + -13832.724609375 + ] + }, + { + "number": 697, + "params": { + "support_angle": 59.7012594635148, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.15276914834976196, + 82.9050064086914, + -19500.27734375 + ] + }, + { + "number": 698, + "params": { + "support_angle": 51.00363779642568, + "tip_thickness": 54.512162128478394 + }, + "values": [ + 0.15732227265834808, + 87.10955810546875, + -20333.8125 + ] + }, + { + "number": 699, + "params": { + "support_angle": 68.49394133538195, + "tip_thickness": 54.20504217801792 + }, + "values": [ + 0.15094968676567078, + 83.39273834228516, + -19172.044921875 + ] + }, + { + "number": 700, + "params": { + "support_angle": 47.69271422006604, + "tip_thickness": 30.60213593333179 + }, + "values": [ + 0.1076105386018753, + 78.19829559326172, + -9036.8642578125 + ] + }, + { + "number": 701, + "params": { + "support_angle": 46.67305459881608, + "tip_thickness": 56.816777491528725 + }, + "values": [ + 0.1596120297908783, + 90.55038452148438, + -20953.17578125 + ] + }, + { + "number": 702, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 53.65791633673592 + }, + "values": [ + 0.15007495880126953, + 82.97464752197266, + -19004.22265625 + ] + }, + { + "number": 703, + "params": { + "support_angle": 33.998954683014205, + "tip_thickness": 46.82813914606159 + }, + "values": [ + 0.13040867447853088, + 72.59244537353516, + -14117.73046875 + ] + }, + { + "number": 705, + "params": { + "support_angle": 35.58610338977741, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11189870536327362, + 72.9891357421875, + -9687.9345703125 + ] + }, + { + "number": 706, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 37.45467419443397 + }, + "values": [ + 0.10489800572395325, + 77.21367645263672, + -8111.05908203125 + ] + }, + { + "number": 707, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 52.31838877718031 + }, + "values": [ + 0.14711108803749084, + 81.77314758300781, + -18435.953125 + ] + }, + { + "number": 709, + "params": { + "support_angle": 64.8394204953006, + "tip_thickness": 47.0966905461395 + }, + "values": [ + 0.130522221326828, + 81.51072692871094, + -15390.96875 + ] + }, + { + "number": 710, + "params": { + "support_angle": 58.122934287034525, + "tip_thickness": 53.084787995758106 + }, + "values": [ + 0.15241937339305878, + 82.51477813720703, + -19436.115234375 + ] + }, + { + "number": 713, + "params": { + "support_angle": 22.043430813323944, + "tip_thickness": 33.31895210865622 + }, + "values": [ + 0.10680562257766724, + 74.70069885253906, + -8416.107421875 + ] + }, + { + "number": 714, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 43.774072687147836 + }, + "values": [ + 0.1237463727593422, + 71.79258728027344, + -12453.8076171875 + ] + }, + { + "number": 715, + "params": { + "support_angle": 59.7012594635148, + "tip_thickness": 55.39356875203555 + }, + "values": [ + 0.15805643796920776, + 85.48007202148438, + -20625.265625 + ] + }, + { + "number": 719, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 51.601096096382236 + }, + "values": [ + 0.14371217787265778, + 81.41648864746094, + -17896.513671875 + ] + }, + { + "number": 720, + "params": { + "support_angle": 45.17086354274284, + "tip_thickness": 49.23442898655892 + }, + "values": [ + 0.13534162938594818, + 75.06350708007812, + -15928.390625 + ] + }, + { + "number": 721, + "params": { + "support_angle": 68.49394133538195, + "tip_thickness": 50.818453334013 + }, + "values": [ + 0.13731521368026733, + 81.34890747070312, + -16762.68359375 + ] + }, + { + "number": 723, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 43.774072687147836 + }, + "values": [ + 0.1237463727593422, + 71.79258728027344, + -12453.8076171875 + ] + }, + { + "number": 724, + "params": { + "support_angle": 32.58995294534763, + "tip_thickness": 35.06805189216494 + }, + "values": [ + 0.10479645431041718, + 76.54315948486328, + -8067.9609375 + ] + }, + { + "number": 725, + "params": { + "support_angle": 53.95511595722448, + "tip_thickness": 53.67513715822015 + }, + "values": [ + 0.1523524671792984, + 82.17040252685547, + -19258.921875 + ] + }, + { + "number": 726, + "params": { + "support_angle": 37.28479967519597, + "tip_thickness": 35.44793150404277 + }, + "values": [ + 0.10452607274055481, + 77.30765533447266, + -8014.27392578125 + ] + }, + { + "number": 728, + "params": { + "support_angle": 31.881877199619986, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11571356654167175, + 71.12530517578125, + -10460.037109375 + ] + }, + { + "number": 729, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 30.86348028940017 + }, + "values": [ + 0.10485177487134933, + 77.58338165283203, + -8133.19287109375 + ] + }, + { + "number": 732, + "params": { + "support_angle": 43.100290182206635, + "tip_thickness": 58.41850018835446 + }, + "values": [ + 0.15758807957172394, + 90.69232177734375, + -20566.603515625 + ] + }, + { + "number": 734, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 54.512162128478394 + }, + "values": [ + 0.15594173967838287, + 84.55963134765625, + -20185.78515625 + ] + }, + { + "number": 737, + "params": { + "support_angle": 66.60591412418063, + "tip_thickness": 42.355227424418054 + }, + "values": [ + 0.12755142152309418, + 81.50721740722656, + -15144.10546875 + ] + }, + { + "number": 738, + "params": { + "support_angle": 48.25665917946044, + "tip_thickness": 50.89952471630677 + }, + "values": [ + 0.13620692491531372, + 74.70133209228516, + -16084.296875 + ] + }, + { + "number": 739, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.1434634029865265, + 81.77892303466797, + -17862.9765625 + ] + }, + { + "number": 740, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 57.67498143531887 + }, + "values": [ + 0.1600317656993866, + 86.8372802734375, + -21076.509765625 + ] + }, + { + "number": 741, + "params": { + "support_angle": 51.67004271583629, + "tip_thickness": 51.217159029401955 + }, + "values": [ + 0.1381048858165741, + 75.03604888916016, + -16495.107421875 + ] + }, + { + "number": 744, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 59.89910511221716 + }, + "values": [ + 0.16226249933242798, + 88.33602142333984, + -21518.818359375 + ] + }, + { + "number": 747, + "params": { + "support_angle": 33.998954683014205, + "tip_thickness": 58.03101044307044 + }, + "values": [ + 0.14916321635246277, + 89.8241195678711, + -18733.9140625 + ] + }, + { + "number": 748, + "params": { + "support_angle": 68.18099885446264, + "tip_thickness": 48.95511636509398 + }, + "values": [ + 0.1327178180217743, + 81.42442321777344, + -15750.109375 + ] + }, + { + "number": 749, + "params": { + "support_angle": 53.95511595722448, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.1458296775817871, + 78.79659271240234, + -18123.43359375 + ] + }, + { + "number": 751, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 55.263567694071256 + }, + "values": [ + 0.15487073361873627, + 84.6832275390625, + -19998.556640625 + ] + }, + { + "number": 752, + "params": { + "support_angle": 66.27842564533881, + "tip_thickness": 43.52518114212396 + }, + "values": [ + 0.128156840801239, + 81.61268615722656, + -15201.29296875 + ] + }, + { + "number": 753, + "params": { + "support_angle": 64.51318919454582, + "tip_thickness": 56.29960807975035 + }, + "values": [ + 0.1586838662624359, + 86.11131286621094, + -20794.4453125 + ] + }, + { + "number": 754, + "params": { + "support_angle": 51.67004271583629, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.1416398137807846, + 76.74089813232422, + -17238.345703125 + ] + }, + { + "number": 758, + "params": { + "support_angle": 57.271973709216496, + "tip_thickness": 34.90967828597113 + }, + "values": [ + 0.11554417014122009, + 76.72625732421875, + -12088.01953125 + ] + }, + { + "number": 759, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 33.739880436098005 + }, + "values": [ + 0.1203760951757431, + 74.61325073242188, + -13651.1064453125 + ] + }, + { + "number": 760, + "params": { + "support_angle": 66.04209413086862, + "tip_thickness": 56.09689086186385 + }, + "values": [ + 0.15739034116268158, + 85.6814193725586, + -20512.939453125 + ] + }, + { + "number": 763, + "params": { + "support_angle": 68.49394133538195, + "tip_thickness": 47.73826307234788 + }, + "values": [ + 0.1312100887298584, + 81.52928161621094, + -15530.5908203125 + ] + }, + { + "number": 769, + "params": { + "support_angle": 39.90843467954717, + "tip_thickness": 58.95080731999538 + }, + "values": [ + 0.1558520793914795, + 90.6578598022461, + -20215.4453125 + ] + }, + { + "number": 770, + "params": { + "support_angle": 46.86978014689615, + "tip_thickness": 57.59566849238281 + }, + "values": [ + 0.15982607007026672, + 90.67547607421875, + -20992.498046875 + ] + }, + { + "number": 772, + "params": { + "support_angle": 37.34766009481139, + "tip_thickness": 34.49752282046322 + }, + "values": [ + 0.10450021922588348, + 77.40702056884766, + -8019.0751953125 + ] + }, + { + "number": 775, + "params": { + "support_angle": 69.31053722398013, + "tip_thickness": 38.42889568776691 + }, + "values": [ + 0.12485501915216446, + 79.44828796386719, + -14766.6728515625 + ] + }, + { + "number": 776, + "params": { + "support_angle": 27.042851018989992, + "tip_thickness": 35.2916095951867 + }, + "values": [ + 0.10674227774143219, + 74.97443389892578, + -8433.017578125 + ] + }, + { + "number": 778, + "params": { + "support_angle": 65.742295053405, + "tip_thickness": 39.670615349241686 + }, + "values": [ + 0.12541961669921875, + 80.07085418701172, + -14839.2119140625 + ] + }, + { + "number": 779, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 40.871816973498994 + }, + "values": [ + 0.1266300529241562, + 81.08016967773438, + -15024.2705078125 + ] + }, + { + "number": 780, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 47.4176505083413 + }, + "values": [ + 0.13089899718761444, + 81.52733612060547, + -15470.0322265625 + ] + }, + { + "number": 781, + "params": { + "support_angle": 51.61321439597652, + "tip_thickness": 30.86348028940017 + }, + "values": [ + 0.10919041931629181, + 78.386962890625, + -9619.0068359375 + ] + }, + { + "number": 782, + "params": { + "support_angle": 51.67004271583629, + "tip_thickness": 30.39283369765001 + }, + "values": [ + 0.10907245427370071, + 78.3899154663086, + -9576.556640625 + ] + }, + { + "number": 783, + "params": { + "support_angle": 22.043430813323944, + "tip_thickness": 35.06805189216494 + }, + "values": [ + 0.10867939889431, + 73.548095703125, + -8804.1142578125 + ] + }, + { + "number": 784, + "params": { + "support_angle": 53.17686860083553, + "tip_thickness": 35.34107900609254 + }, + "values": [ + 0.11247088015079498, + 78.02755737304688, + -10875.494140625 + ] + }, + { + "number": 785, + "params": { + "support_angle": 68.05351587347275, + "tip_thickness": 34.45988183259339 + }, + "values": [ + 0.12090213596820831, + 75.02621459960938, + -13844.0107421875 + ] + }, + { + "number": 786, + "params": { + "support_angle": 40.73120618635119, + "tip_thickness": 32.56049004235932 + }, + "values": [ + 0.10531853139400482, + 77.60629272460938, + -8276.810546875 + ] + }, + { + "number": 787, + "params": { + "support_angle": 69.84371259229736, + "tip_thickness": 45.065850309937275 + }, + "values": [ + 0.12939335405826569, + 81.67346954345703, + -15338.4609375 + ] + }, + { + "number": 790, + "params": { + "support_angle": 66.60591412418063, + "tip_thickness": 52.498814111975335 + }, + "values": [ + 0.14515471458435059, + 81.95021057128906, + -18130.939453125 + ] + }, + { + "number": 791, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 37.27411510904189 + }, + "values": [ + 0.12275727838277817, + 77.2940673828125, + -14324.29296875 + ] + }, + { + "number": 793, + "params": { + "support_angle": 30.25698202410036, + "tip_thickness": 35.720631619909994 + }, + "values": [ + 0.10591938346624374, + 75.72047424316406, + -8294.9921875 + ] + }, + { + "number": 794, + "params": { + "support_angle": 21.827483392404744, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12002021074295044, + 73.41165161132812, + -11405.296875 + ] + }, + { + "number": 796, + "params": { + "support_angle": 43.60334725549996, + "tip_thickness": 46.94523399787849 + }, + "values": [ + 0.13078315556049347, + 75.80097961425781, + -14543.5439453125 + ] + }, + { + "number": 797, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12650717794895172, + 80.9926986694336, + -15000.671875 + ] + }, + { + "number": 800, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 50.818453334013 + }, + "values": [ + 0.13781754672527313, + 81.27391052246094, + -16872.4140625 + ] + }, + { + "number": 801, + "params": { + "support_angle": 42.664441737401376, + "tip_thickness": 45.7317080798274 + }, + "values": [ + 0.12973473966121674, + 75.01803588867188, + -14289.1396484375 + ] + }, + { + "number": 802, + "params": { + "support_angle": 53.95511595722448, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.1458296775817871, + 78.79659271240234, + -18123.43359375 + ] + }, + { + "number": 803, + "params": { + "support_angle": 42.03813734691141, + "tip_thickness": 42.02289182625978 + }, + "values": [ + 0.11053705960512161, + 76.13753509521484, + -9644.87890625 + ] + }, + { + "number": 805, + "params": { + "support_angle": 37.31729971829806, + "tip_thickness": 46.78920993924653 + }, + "values": [ + 0.13106882572174072, + 71.80416107177734, + -14476.3759765625 + ] + }, + { + "number": 806, + "params": { + "support_angle": 27.762012296535623, + "tip_thickness": 35.45784391485813 + }, + "values": [ + 0.10662218928337097, + 75.10022735595703, + -8413.99609375 + ] + }, + { + "number": 811, + "params": { + "support_angle": 33.998954683014205, + "tip_thickness": 30.761590669810346 + }, + "values": [ + 0.10410928726196289, + 77.422119140625, + -7907.8017578125 + ] + }, + { + "number": 813, + "params": { + "support_angle": 52.94619709550757, + "tip_thickness": 49.955916620886 + }, + "values": [ + 0.13981789350509644, + 78.38451385498047, + -16974.640625 + ] + }, + { + "number": 815, + "params": { + "support_angle": 31.098608096647474, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.10414905846118927, + 76.92345428466797, + -7910.5888671875 + ] + }, + { + "number": 817, + "params": { + "support_angle": 53.59463717993642, + "tip_thickness": 30.591316132630926 + }, + "values": [ + 0.10987343639135361, + 78.37320709228516, + -9875.37109375 + ] + }, + { + "number": 819, + "params": { + "support_angle": 28.92723310271668, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11719414591789246, + 70.90193176269531, + -10845.921875 + ] + }, + { + "number": 820, + "params": { + "support_angle": 52.63730539259374, + "tip_thickness": 37.27411510904189 + }, + "values": [ + 0.1144215315580368, + 77.41716766357422, + -11612.0771484375 + ] + }, + { + "number": 821, + "params": { + "support_angle": 31.909139052336325, + "tip_thickness": 32.983241782780354 + }, + "values": [ + 0.10420280694961548, + 76.91958618164062, + -7924.5380859375 + ] + }, + { + "number": 823, + "params": { + "support_angle": 68.49394133538195, + "tip_thickness": 51.66800795566978 + }, + "values": [ + 0.1401432603597641, + 81.51410675048828, + -17310.087890625 + ] + }, + { + "number": 824, + "params": { + "support_angle": 59.86476828897768, + "tip_thickness": 48.95511636509398 + }, + "values": [ + 0.13378450274467468, + 80.94120788574219, + -15959.28515625 + ] + }, + { + "number": 826, + "params": { + "support_angle": 62.784823405314285, + "tip_thickness": 54.90659593700975 + }, + "values": [ + 0.1563117504119873, + 84.91714477539062, + -20279.07421875 + ] + }, + { + "number": 827, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 30.86348028940017 + }, + "values": [ + 0.11419215053319931, + 77.0737533569336, + -11596.681640625 + ] + }, + { + "number": 828, + "params": { + "support_angle": 64.51318919454582, + "tip_thickness": 56.29960807975035 + }, + "values": [ + 0.1586838662624359, + 86.11131286621094, + -20794.4453125 + ] + }, + { + "number": 830, + "params": { + "support_angle": 45.17086354274284, + "tip_thickness": 49.23442898655892 + }, + "values": [ + 0.13534162938594818, + 75.06350708007812, + -15928.390625 + ] + }, + { + "number": 832, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 36.149528862474625 + }, + "values": [ + 0.1216576099395752, + 75.95594024658203, + -14017.6298828125 + ] + }, + { + "number": 833, + "params": { + "support_angle": 47.69271422006604, + "tip_thickness": 38.7944319078304 + }, + "values": [ + 0.11136096715927124, + 78.20550537109375, + -10380.97265625 + ] + }, + { + "number": 834, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 56.89007455563359 + }, + "values": [ + 0.15361621975898743, + 90.19905853271484, + -19736.859375 + ] + }, + { + "number": 835, + "params": { + "support_angle": 20.650096175536802, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12047207355499268, + 74.22393798828125, + -11492.3203125 + ] + }, + { + "number": 836, + "params": { + "support_angle": 24.2754265427234, + "tip_thickness": 36.23658765438082 + }, + "values": [ + 0.10930138826370239, + 73.28162384033203, + -8953.19921875 + ] + }, + { + "number": 837, + "params": { + "support_angle": 21.326610193690986, + "tip_thickness": 35.443063052693915 + }, + "values": [ + 0.10956542938947678, + 72.99392700195312, + -8993.1708984375 + ] + }, + { + "number": 839, + "params": { + "support_angle": 30.49527965477929, + "tip_thickness": 42.642736517774935 + }, + "values": [ + 0.12258896976709366, + 73.41275024414062, + -12103.130859375 + ] + }, + { + "number": 840, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 37.27411510904189 + }, + "values": [ + 0.12275727838277817, + 77.2940673828125, + -14324.29296875 + ] + }, + { + "number": 841, + "params": { + "support_angle": 47.69271422006604, + "tip_thickness": 56.780151332309245 + }, + "values": [ + 0.15996679663658142, + 90.46253204345703, + -21022.65234375 + ] + }, + { + "number": 842, + "params": { + "support_angle": 60.87217808692205, + "tip_thickness": 40.25452055093628 + }, + "values": [ + 0.1247762143611908, + 79.52765655517578, + -14667.94921875 + ] + }, + { + "number": 843, + "params": { + "support_angle": 63.275356294699016, + "tip_thickness": 54.512162128478394 + }, + "values": [ + 0.15501917898654938, + 84.37202453613281, + -20010.48046875 + ] + }, + { + "number": 844, + "params": { + "support_angle": 32.971171671564626, + "tip_thickness": 40.92809583144226 + }, + "values": [ + 0.11582048237323761, + 71.19866943359375, + -10493.541015625 + ] + }, + { + "number": 846, + "params": { + "support_angle": 38.71353978780601, + "tip_thickness": 47.70884827544423 + }, + "values": [ + 0.13232971727848053, + 72.88440704345703, + -14903.0986328125 + ] + }, + { + "number": 847, + "params": { + "support_angle": 34.43152766201279, + "tip_thickness": 38.041909224686286 + }, + "values": [ + 0.10637390613555908, + 75.70270538330078, + -8448.318359375 + ] + }, + { + "number": 848, + "params": { + "support_angle": 53.58503422029283, + "tip_thickness": 47.43714664267837 + }, + "values": [ + 0.13078860938549042, + 80.78026580810547, + -15150.64453125 + ] + }, + { + "number": 849, + "params": { + "support_angle": 30.25698202410036, + "tip_thickness": 35.720631619909994 + }, + "values": [ + 0.10591938346624374, + 75.72047424316406, + -8294.9921875 + ] + }, + { + "number": 850, + "params": { + "support_angle": 30.316685920289625, + "tip_thickness": 40.92809583144226 + }, + "values": [ + 0.11720094084739685, + 70.90044403076172, + -10845.083984375 + ] + }, + { + "number": 853, + "params": { + "support_angle": 40.47058260956202, + "tip_thickness": 46.5614154255594 + }, + "values": [ + 0.1308855414390564, + 72.86363220214844, + -14549.1923828125 + ] + }, + { + "number": 854, + "params": { + "support_angle": 41.80632645676583, + "tip_thickness": 54.512162128478394 + }, + "values": [ + 0.15529708564281464, + 90.03584289550781, + -20104.3359375 + ] + }, + { + "number": 855, + "params": { + "support_angle": 34.723287977095886, + "tip_thickness": 41.19927948839259 + }, + "values": [ + 0.11558296531438828, + 71.47608947753906, + -10443.92578125 + ] + }, + { + "number": 856, + "params": { + "support_angle": 67.42266534810784, + "tip_thickness": 52.908173824792826 + }, + "values": [ + 0.14652767777442932, + 82.21504211425781, + -18342.419921875 + ] + }, + { + "number": 859, + "params": { + "support_angle": 60.87217808692205, + "tip_thickness": 40.25452055093628 + }, + "values": [ + 0.1247762143611908, + 79.52765655517578, + -14667.94921875 + ] + }, + { + "number": 861, + "params": { + "support_angle": 64.72761134470457, + "tip_thickness": 53.995657678419455 + }, + "values": [ + 0.15271984040737152, + 83.55927276611328, + -19529.1640625 + ] + }, + { + "number": 862, + "params": { + "support_angle": 41.260675223461675, + "tip_thickness": 54.90659593700975 + }, + "values": [ + 0.15507447719573975, + 90.08963012695312, + -20047.9765625 + ] + }, + { + "number": 863, + "params": { + "support_angle": 68.49394133538195, + "tip_thickness": 50.818453334013 + }, + "values": [ + 0.13731521368026733, + 81.34890747070312, + -16762.68359375 + ] + }, + { + "number": 864, + "params": { + "support_angle": 53.58503422029283, + "tip_thickness": 30.674079249603523 + }, + "values": [ + 0.10989919304847717, + 78.37155151367188, + -9884.6337890625 + ] + }, + { + "number": 866, + "params": { + "support_angle": 47.081710733043344, + "tip_thickness": 53.65791633673592 + }, + "values": [ + 0.1580560803413391, + 89.15805053710938, + -20618.37890625 + ] + }, + { + "number": 869, + "params": { + "support_angle": 46.63086332275115, + "tip_thickness": 37.27411510904189 + }, + "values": [ + 0.1091788038611412, + 78.11178588867188, + -9565.4375 + ] + }, + { + "number": 871, + "params": { + "support_angle": 68.49394133538195, + "tip_thickness": 50.818453334013 + }, + "values": [ + 0.13731521368026733, + 81.34890747070312, + -16762.68359375 + ] + }, + { + "number": 872, + "params": { + "support_angle": 61.74651183996495, + "tip_thickness": 59.532065421106566 + }, + "values": [ + 0.16206704080104828, + 88.31178283691406, + -21478.91015625 + ] + }, + { + "number": 873, + "params": { + "support_angle": 46.28450911513429, + "tip_thickness": 35.1503785754491 + }, + "values": [ + 0.10802841931581497, + 78.10365295410156, + -9160.07421875 + ] + }, + { + "number": 874, + "params": { + "support_angle": 33.61536632596845, + "tip_thickness": 30.551720296424 + }, + "values": [ + 0.10407653450965881, + 77.40899658203125, + -7897.16064453125 + ] + }, + { + "number": 877, + "params": { + "support_angle": 65.41329429833269, + "tip_thickness": 38.22165662061618 + }, + "values": [ + 0.12326452881097794, + 77.94132995605469, + -14432.3408203125 + ] + }, + { + "number": 878, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 46.6253400754738 + }, + "values": [ + 0.13021649420261383, + 81.58629608154297, + -15372.84765625 + ] + }, + { + "number": 880, + "params": { + "support_angle": 61.48709018536008, + "tip_thickness": 52.22305853262613 + }, + "values": [ + 0.14746971428394318, + 81.63372802734375, + -18502.51171875 + ] + }, + { + "number": 882, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 30.329874859754415 + }, + "values": [ + 0.11678888648748398, + 75.78289794921875, + -12652.3916015625 + ] + }, + { + "number": 883, + "params": { + "support_angle": 46.63086332275115, + "tip_thickness": 49.955916620886 + }, + "values": [ + 0.1349552720785141, + 74.50750732421875, + -15841.83984375 + ] + }, + { + "number": 884, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 34.10656890206422 + }, + "values": [ + 0.12059026211500168, + 74.70686340332031, + -13727.1083984375 + ] + }, + { + "number": 885, + "params": { + "support_angle": 65.00093209240526, + "tip_thickness": 56.216702326875456 + }, + "values": [ + 0.15823142230510712, + 85.95683288574219, + -20695.69921875 + ] + }, + { + "number": 886, + "params": { + "support_angle": 57.271973709216496, + "tip_thickness": 47.92239306510925 + }, + "values": [ + 0.13148142397403717, + 81.0319595336914, + -15394.5009765625 + ] + }, + { + "number": 887, + "params": { + "support_angle": 53.58503422029283, + "tip_thickness": 52.65411767020857 + }, + "values": [ + 0.14728420972824097, + 79.42549896240234, + -18361.30078125 + ] + }, + { + "number": 888, + "params": { + "support_angle": 50.02584302168267, + "tip_thickness": 49.95110023638766 + }, + "values": [ + 0.1347406655550003, + 74.82428741455078, + -15726.6669921875 + ] + }, + { + "number": 889, + "params": { + "support_angle": 59.7012594635148, + "tip_thickness": 55.39356875203555 + }, + "values": [ + 0.15805643796920776, + 85.48007202148438, + -20625.265625 + ] + }, + { + "number": 892, + "params": { + "support_angle": 39.156926412474874, + "tip_thickness": 45.567531158485124 + }, + "values": [ + 0.1284741312265396, + 71.8985595703125, + -13873.818359375 + ] + }, + { + "number": 893, + "params": { + "support_angle": 66.04209413086862, + "tip_thickness": 50.818453334013 + }, + "values": [ + 0.1381913125514984, + 81.22563171386719, + -16952.587890625 + ] + }, + { + "number": 894, + "params": { + "support_angle": 62.784823405314285, + "tip_thickness": 54.90659593700975 + }, + "values": [ + 0.1563117504119873, + 84.91714477539062, + -20279.07421875 + ] + }, + { + "number": 895, + "params": { + "support_angle": 67.30577310668164, + "tip_thickness": 31.408979003251652 + }, + "values": [ + 0.1184103786945343, + 75.0614242553711, + -13121.4384765625 + ] + }, + { + "number": 898, + "params": { + "support_angle": 32.26745548406605, + "tip_thickness": 41.678868421259295 + }, + "values": [ + 0.11880151927471161, + 71.01521301269531, + -11277.845703125 + ] + }, + { + "number": 899, + "params": { + "support_angle": 34.434686838534994, + "tip_thickness": 40.670181493948476 + }, + "values": [ + 0.11340099573135376, + 72.21601104736328, + -9942.2265625 + ] + }, + { + "number": 902, + "params": { + "support_angle": 37.31729971829806, + "tip_thickness": 46.99213920890692 + }, + "values": [ + 0.13164637982845306, + 71.89810180664062, + -14603.0751953125 + ] + }, + { + "number": 903, + "params": { + "support_angle": 43.80252010995498, + "tip_thickness": 49.91013496087945 + }, + "values": [ + 0.1422853171825409, + 81.79602813720703, + -17492.849609375 + ] + }, + { + "number": 904, + "params": { + "support_angle": 64.72761134470457, + "tip_thickness": 31.408979003251652 + }, + "values": [ + 0.11676844209432602, + 75.8590316772461, + -12632.30859375 + ] + }, + { + "number": 905, + "params": { + "support_angle": 34.434686838534994, + "tip_thickness": 42.02289182625978 + }, + "values": [ + 0.11913862824440002, + 71.04100036621094, + -11358.1943359375 + ] + }, + { + "number": 907, + "params": { + "support_angle": 56.62860486051252, + "tip_thickness": 53.995657678419455 + }, + "values": [ + 0.1549854576587677, + 83.58224487304688, + -19898.5390625 + ] + }, + { + "number": 909, + "params": { + "support_angle": 21.559156753064233, + "tip_thickness": 37.867921328994754 + }, + "values": [ + 0.11386047303676605, + 71.16118621826172, + -9942.5029296875 + ] + }, + { + "number": 913, + "params": { + "support_angle": 36.712194540848046, + "tip_thickness": 59.89910511221716 + }, + "values": [ + 0.15353137254714966, + 90.4761962890625, + -19724.845703125 + ] + }, + { + "number": 914, + "params": { + "support_angle": 58.545610187291096, + "tip_thickness": 56.29960807975035 + }, + "values": [ + 0.15946364402770996, + 86.46420288085938, + -20925.9140625 + ] + }, + { + "number": 915, + "params": { + "support_angle": 25.32991265668859, + "tip_thickness": 32.254133452074264 + }, + "values": [ + 0.10516189783811569, + 76.03214263916016, + -8103.01171875 + ] + }, + { + "number": 916, + "params": { + "support_angle": 53.58503422029283, + "tip_thickness": 51.8456626861081 + }, + "values": [ + 0.14347243309020996, + 77.67395782470703, + -17667.732421875 + ] + }, + { + "number": 918, + "params": { + "support_angle": 47.69271422006604, + "tip_thickness": 50.65207189283208 + }, + "values": [ + 0.13604527711868286, + 74.77131652832031, + -16059.4921875 + ] + }, + { + "number": 919, + "params": { + "support_angle": 41.74136693018731, + "tip_thickness": 37.3920609971732 + }, + "values": [ + 0.10593414306640625, + 77.51534271240234, + -8454.3369140625 + ] + }, + { + "number": 920, + "params": { + "support_angle": 67.42266534810784, + "tip_thickness": 52.908173824792826 + }, + "values": [ + 0.14652767777442932, + 82.21504211425781, + -18342.419921875 + ] + }, + { + "number": 921, + "params": { + "support_angle": 60.955115883709986, + "tip_thickness": 53.982476369069374 + }, + "values": [ + 0.15467047691345215, + 83.89037322998047, + -19890.126953125 + ] + }, + { + "number": 922, + "params": { + "support_angle": 57.271973709216496, + "tip_thickness": 50.8408941256328 + }, + "values": [ + 0.1437786966562271, + 80.47013854980469, + -17854.833984375 + ] + }, + { + "number": 923, + "params": { + "support_angle": 25.601945108402617, + "tip_thickness": 38.16435411689888 + }, + "values": [ + 0.1125941276550293, + 71.70714569091797, + -9640.8525390625 + ] + }, + { + "number": 924, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 31.408979003251652 + }, + "values": [ + 0.11820609122514725, + 75.15128326416016, + -13064.9873046875 + ] + }, + { + "number": 925, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 47.706920006072615 + }, + "values": [ + 0.13116323947906494, + 81.49514770507812, + -15503.23828125 + ] + }, + { + "number": 926, + "params": { + "support_angle": 38.04869484700134, + "tip_thickness": 32.747462199799024 + }, + "values": [ + 0.10466903448104858, + 77.52906036376953, + -8072.8896484375 + ] + }, + { + "number": 927, + "params": { + "support_angle": 65.32360554822735, + "tip_thickness": 57.51940726386728 + }, + "values": [ + 0.16042973101139069, + 86.96709442138672, + -21157.01171875 + ] + }, + { + "number": 928, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12650717794895172, + 80.9926986694336, + -15000.671875 + ] + }, + { + "number": 930, + "params": { + "support_angle": 67.51186769104012, + "tip_thickness": 30.86348028940017 + }, + "values": [ + 0.11786486208438873, + 75.26602935791016, + -12977.7021484375 + ] + }, + { + "number": 931, + "params": { + "support_angle": 42.30028864789779, + "tip_thickness": 45.567531158485124 + }, + "values": [ + 0.12986868619918823, + 74.45329284667969, + -14377.5087890625 + ] + }, + { + "number": 933, + "params": { + "support_angle": 64.51318919454582, + "tip_thickness": 30.86348028940017 + }, + "values": [ + 0.11608865857124329, + 76.16351318359375, + -12392.919921875 + ] + }, + { + "number": 934, + "params": { + "support_angle": 59.86476828897768, + "tip_thickness": 56.188375076292246 + }, + "values": [ + 0.1592693328857422, + 86.24784088134766, + -20884.58984375 + ] + }, + { + "number": 935, + "params": { + "support_angle": 42.03813734691141, + "tip_thickness": 38.22165662061618 + }, + "values": [ + 0.10622110217809677, + 77.49248504638672, + -8539.1240234375 + ] + }, + { + "number": 936, + "params": { + "support_angle": 66.63642416770065, + "tip_thickness": 51.96674659228684 + }, + "values": [ + 0.14252671599388123, + 81.65093994140625, + -17711.646484375 + ] + }, + { + "number": 938, + "params": { + "support_angle": 60.328057393072484, + "tip_thickness": 49.76350100132152 + }, + "values": [ + 0.13664141297340393, + 80.81363677978516, + -16579.0390625 + ] + }, + { + "number": 939, + "params": { + "support_angle": 51.67004271583629, + "tip_thickness": 50.768296935535574 + }, + "values": [ + 0.1380477398633957, + 75.58126831054688, + -16491.1015625 + ] + }, + { + "number": 940, + "params": { + "support_angle": 62.459782578265965, + "tip_thickness": 37.490040265775576 + }, + "values": [ + 0.12160041183233261, + 75.96955108642578, + -13942.037109375 + ] + }, + { + "number": 943, + "params": { + "support_angle": 20.650096175536802, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.12047207355499268, + 74.22393798828125, + -11492.3203125 + ] + }, + { + "number": 945, + "params": { + "support_angle": 69.38340039983234, + "tip_thickness": 36.93224389764214 + }, + "values": [ + 0.1230347529053688, + 77.62598419189453, + -14420.2373046875 + ] + }, + { + "number": 947, + "params": { + "support_angle": 67.20296698433066, + "tip_thickness": 30.329874859754415 + }, + "values": [ + 0.11708056926727295, + 75.61697387695312, + -12751.62890625 + ] + }, + { + "number": 948, + "params": { + "support_angle": 30.316685920289625, + "tip_thickness": 40.92809583144226 + }, + "values": [ + 0.11720094084739685, + 70.90044403076172, + -10845.083984375 + ] + }, + { + "number": 952, + "params": { + "support_angle": 29.028767256366677, + "tip_thickness": 40.92809583144226 + }, + "values": [ + 0.11780442297458649, + 70.9554214477539, + -11014.013671875 + ] + }, + { + "number": 954, + "params": { + "support_angle": 21.697298929289943, + "tip_thickness": 36.448211357722705 + }, + "values": [ + 0.11114662885665894, + 72.15209197998047, + -9320.728515625 + ] + }, + { + "number": 955, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 59.47135850725819 + }, + "values": [ + 0.1620153933763504, + 87.7791748046875, + -21470.1875 + ] + }, + { + "number": 957, + "params": { + "support_angle": 42.03813734691141, + "tip_thickness": 57.73188586129591 + }, + "values": [ + 0.15686386823654175, + 90.63182067871094, + -20427.681640625 + ] + }, + { + "number": 958, + "params": { + "support_angle": 68.55291225826838, + "tip_thickness": 58.32799467340302 + }, + "values": [ + 0.16034558415412903, + 87.04602813720703, + -21142.86328125 + ] + }, + { + "number": 959, + "params": { + "support_angle": 64.51318919454582, + "tip_thickness": 30.86348028940017 + }, + "values": [ + 0.11608865857124329, + 76.16351318359375, + -12392.919921875 + ] + }, + { + "number": 960, + "params": { + "support_angle": 43.71071083287319, + "tip_thickness": 55.86127952967941 + }, + "values": [ + 0.15733692049980164, + 90.368896484375, + -20531.884765625 + ] + }, + { + "number": 961, + "params": { + "support_angle": 47.69271422006604, + "tip_thickness": 55.33648195605211 + }, + "values": [ + 0.15947729349136353, + 90.05613708496094, + -20933.80078125 + ] + }, + { + "number": 963, + "params": { + "support_angle": 35.58610338977741, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.11189870536327362, + 72.9891357421875, + -9687.9345703125 + ] + }, + { + "number": 964, + "params": { + "support_angle": 61.44577370753387, + "tip_thickness": 31.110229041464777 + }, + "values": [ + 0.11442738026380539, + 76.98485565185547, + -11688.68359375 + ] + }, + { + "number": 965, + "params": { + "support_angle": 64.51318919454582, + "tip_thickness": 47.888096354461595 + }, + "values": [ + 0.13132229447364807, + 81.42615509033203, + -15497.673828125 + ] + }, + { + "number": 966, + "params": { + "support_angle": 31.500441864385152, + "tip_thickness": 33.61700657331836 + }, + "values": [ + 0.10445050895214081, + 76.72071075439453, + -7981.1640625 + ] + }, + { + "number": 967, + "params": { + "support_angle": 62.824492059416116, + "tip_thickness": 32.308596048876275 + }, + "values": [ + 0.11655531823635101, + 76.03302001953125, + -12543.7021484375 + ] + }, + { + "number": 968, + "params": { + "support_angle": 22.043430813323944, + "tip_thickness": 35.06805189216494 + }, + "values": [ + 0.10867939889431, + 73.548095703125, + -8804.1142578125 + ] + }, + { + "number": 969, + "params": { + "support_angle": 47.081710733043344, + "tip_thickness": 50.88866327634419 + }, + "values": [ + 0.13974963128566742, + 77.37632751464844, + -16923.1953125 + ] + }, + { + "number": 970, + "params": { + "support_angle": 30.316685920289625, + "tip_thickness": 40.92809583144226 + }, + "values": [ + 0.11720094084739685, + 70.90044403076172, + -10845.083984375 + ] + }, + { + "number": 972, + "params": { + "support_angle": 34.434686838534994, + "tip_thickness": 31.96069022396827 + }, + "values": [ + 0.10414966940879822, + 77.36910247802734, + -7913.6533203125 + ] + }, + { + "number": 973, + "params": { + "support_angle": 35.76451689153052, + "tip_thickness": 57.51940726386728 + }, + "values": [ + 0.1506248414516449, + 89.95567321777344, + -19073.404296875 + ] + }, + { + "number": 974, + "params": { + "support_angle": 46.97456461876686, + "tip_thickness": 31.408979003251652 + }, + "values": [ + 0.10749681293964386, + 78.1490478515625, + -8992.3603515625 + ] + }, + { + "number": 976, + "params": { + "support_angle": 47.69271422006604, + "tip_thickness": 48.77674129311028 + }, + "values": [ + 0.13156761229038239, + 74.79754638671875, + -15041.5732421875 + ] + }, + { + "number": 977, + "params": { + "support_angle": 64.29888741180935, + "tip_thickness": 53.995657678419455 + }, + "values": [ + 0.15297085046768188, + 83.60755920410156, + -19580.55859375 + ] + }, + { + "number": 979, + "params": { + "support_angle": 57.271973709216496, + "tip_thickness": 48.47589564546914 + }, + "values": [ + 0.13296431303024292, + 80.84526062011719, + -15700.1962890625 + ] + }, + { + "number": 980, + "params": { + "support_angle": 46.28450911513429, + "tip_thickness": 36.988784242609015 + }, + "values": [ + 0.10876712203025818, + 78.07669067382812, + -9413.28125 + ] + }, + { + "number": 981, + "params": { + "support_angle": 21.2200390778269, + "tip_thickness": 30.86348028940017 + }, + "values": [ + 0.10546356439590454, + 75.69329833984375, + -8138.05810546875 + ] + }, + { + "number": 982, + "params": { + "support_angle": 57.271973709216496, + "tip_thickness": 50.8408941256328 + }, + "values": [ + 0.1437786966562271, + 80.47013854980469, + -17854.833984375 + ] + }, + { + "number": 983, + "params": { + "support_angle": 63.50494369504649, + "tip_thickness": 30.638082325511615 + }, + "values": [ + 0.11527933180332184, + 76.56289672851562, + -12047.8427734375 + ] + }, + { + "number": 984, + "params": { + "support_angle": 37.31729971829806, + "tip_thickness": 56.24105018052598 + }, + "values": [ + 0.15120714902877808, + 89.86896514892578, + -19201.13671875 + ] + }, + { + "number": 985, + "params": { + "support_angle": 57.271973709216496, + "tip_thickness": 34.90967828597113 + }, + "values": [ + 0.11554417014122009, + 76.72625732421875, + -12088.01953125 + ] + }, + { + "number": 986, + "params": { + "support_angle": 64.51318919454582, + "tip_thickness": 45.868114020816364 + }, + "values": [ + 0.1296079307794571, + 81.62776947021484, + -15277.6708984375 + ] + }, + { + "number": 987, + "params": { + "support_angle": 56.62860486051252, + "tip_thickness": 53.995657678419455 + }, + "values": [ + 0.1549854576587677, + 83.58224487304688, + -19898.5390625 + ] + }, + { + "number": 989, + "params": { + "support_angle": 59.86476828897768, + "tip_thickness": 53.963497073209965 + }, + "values": [ + 0.15499021112918854, + 83.86964416503906, + -19956.359375 + ] + }, + { + "number": 990, + "params": { + "support_angle": 34.434686838534994, + "tip_thickness": 40.670181493948476 + }, + "values": [ + 0.11340099573135376, + 72.21601104736328, + -9942.2265625 + ] + }, + { + "number": 991, + "params": { + "support_angle": 69.89670552666686, + "tip_thickness": 40.92809583144226 + }, + "values": [ + 0.12705522775650024, + 81.28678131103516, + -15120.1171875 + ] + }, + { + "number": 993, + "params": { + "support_angle": 47.69271422006604, + "tip_thickness": 49.955916620886 + }, + "values": [ + 0.13380876183509827, + 73.63969421386719, + -15561.640625 + ] + }, + { + "number": 994, + "params": { + "support_angle": 40.09654568046211, + "tip_thickness": 35.06805189216494 + }, + "values": [ + 0.10515701770782471, + 77.50540924072266, + -8214.9072265625 + ] + }, + { + "number": 995, + "params": { + "support_angle": 43.99378101519544, + "tip_thickness": 48.8251638965511 + }, + "values": [ + 0.1352059543132782, + 76.11914825439453, + -16029.4208984375 + ] + }, + { + "number": 996, + "params": { + "support_angle": 63.68385570931694, + "tip_thickness": 34.90967828597113 + }, + "values": [ + 0.12033674865961075, + 74.67765808105469, + -13610.359375 + ] + }, + { + "number": 997, + "params": { + "support_angle": 69.20417345996475, + "tip_thickness": 49.955916620886 + }, + "values": [ + 0.1347755342721939, + 81.36328125, + -16191.861328125 + ] + }, + { + "number": 998, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 53.04820241593555 + }, + "values": [ + 0.14748279750347137, + 82.33216094970703, + -18511.609375 + ] + } + ] +} \ No newline at end of file diff --git a/studies/bracket_pareto_3obj/2_results/nn_study.db b/studies/bracket_pareto_3obj/2_results/nn_study.db new file mode 100644 index 00000000..1edc2eb4 Binary files /dev/null and b/studies/bracket_pareto_3obj/2_results/nn_study.db differ diff --git a/studies/bracket_pareto_3obj/2_results/pareto_front.json b/studies/bracket_pareto_3obj/2_results/pareto_front.json new file mode 100644 index 00000000..64c59cd6 --- /dev/null +++ b/studies/bracket_pareto_3obj/2_results/pareto_front.json @@ -0,0 +1,1013 @@ +{ + "n_pareto": 84, + "trials": [ + { + "number": 0, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 1, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 47.959754525911094 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 2, + "params": { + "support_angle": 27.800932022121827, + "tip_thickness": 34.67983561008608 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 3, + "params": { + "support_angle": 22.904180608409973, + "tip_thickness": 55.98528437324806 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 4, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 5, + "params": { + "support_angle": 21.02922471479012, + "tip_thickness": 59.097295564859834 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 6, + "params": { + "support_angle": 61.622132040021086, + "tip_thickness": 36.370173320348286 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 7, + "params": { + "support_angle": 29.09124836035503, + "tip_thickness": 35.50213529560301 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 8, + "params": { + "support_angle": 35.21211214797688, + "tip_thickness": 45.74269294896713 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 9, + "params": { + "support_angle": 41.59725093210579, + "tip_thickness": 38.736874205941255 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 10, + "params": { + "support_angle": 50.59264473611897, + "tip_thickness": 34.18481581956125 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 11, + "params": { + "support_angle": 34.60723242676091, + "tip_thickness": 40.99085529881075 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 12, + "params": { + "support_angle": 42.8034992108518, + "tip_thickness": 53.55527884179041 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 14, + "params": { + "support_angle": 49.620728443102124, + "tip_thickness": 31.393512381599933 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 15, + "params": { + "support_angle": 50.37724259507192, + "tip_thickness": 35.115723710618745 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 16, + "params": { + "support_angle": 23.252579649263975, + "tip_thickness": 58.466566117599996 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ] + }, + { + "number": 18, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.1594800904665372, + 89.4502578125, + -20960.59592691965 + ] + }, + { + "number": 20, + "params": { + "support_angle": 27.800932022121827, + "tip_thickness": 34.67983561008608 + }, + "values": [ + 0.10630918746092984, + 75.5471015625, + -8142.120566330409 + ] + }, + { + "number": 22, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 36.370173320348286 + }, + "values": [ + 0.11780122136337835, + 72.6732421875, + -12366.875819243376 + ] + }, + { + "number": 23, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 35.115723710618745 + }, + "values": [ + 0.11313847406441968, + 75.261375, + -10673.210765604706 + ] + }, + { + "number": 24, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14318368576930707, + 73.3545859375, + -17662.840771637857 + ] + }, + { + "number": 25, + "params": { + "support_angle": 55.40362888980228, + "tip_thickness": 30.617534828874074 + }, + "values": [ + 0.10943751404355549, + 84.311, + -10332.376675469583 + ] + }, + { + "number": 26, + "params": { + "support_angle": 50.59264473611897, + "tip_thickness": 59.097295564859834 + }, + "values": [ + 0.16085535241637244, + 88.5342734375, + -21249.113101276806 + ] + }, + { + "number": 28, + "params": { + "support_angle": 29.09124836035503, + "tip_thickness": 35.50213529560301 + }, + "values": [ + 0.10827058249925942, + 72.4169921875, + -8632.595914157022 + ] + }, + { + "number": 29, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.1594800904665372, + 89.4502578125, + -20960.59592691965 + ] + }, + { + "number": 33, + "params": { + "support_angle": 29.09124836035503, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14251897231791152, + 92.731484375, + -16946.136144219086 + ] + }, + { + "number": 34, + "params": { + "support_angle": 34.5614570099021, + "tip_thickness": 38.736874205941255 + }, + "values": [ + 0.11586110588460208, + 70.7065546875, + -10587.654595231894 + ] + }, + { + "number": 35, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 48.35558684167138 + }, + "values": [ + 0.13786499255161738, + 79.9088359375, + -16547.824880313816 + ] + }, + { + "number": 36, + "params": { + "support_angle": 41.59725093210579, + "tip_thickness": 34.18481581956125 + }, + "values": [ + 0.10873759473302179, + 75.999984375, + -9102.751265315374 + ] + }, + { + "number": 38, + "params": { + "support_angle": 38.318092164684586, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14271498947987588, + 89.380015625, + -17107.99153151556 + ] + }, + { + "number": 40, + "params": { + "support_angle": 50.59264473611897, + "tip_thickness": 34.18481581956125 + }, + "values": [ + 0.11190565081078178, + 76.7876328125, + -10422.469553635548 + ] + }, + { + "number": 42, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 35.990213464750795 + }, + "values": [ + 0.11728015145153212, + 81.8612734375, + -12173.339608456614 + ] + }, + { + "number": 44, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 36.370173320348286 + }, + "values": [ + 0.11778844081157942, + 72.7532421875, + -12365.5598495581 + ] + }, + { + "number": 46, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 31.393512381599933 + }, + "values": [ + 0.11118880708784562, + 84.8740390625, + -10878.054955936728 + ] + }, + { + "number": 47, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14318368576930707, + 73.3545859375, + -17662.840771637857 + ] + }, + { + "number": 48, + "params": { + "support_angle": 50.37724259507192, + "tip_thickness": 35.115723710618745 + }, + "values": [ + 0.11326420640721613, + 75.4833046875, + -10722.26724748861 + ] + }, + { + "number": 49, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 31.951547789558386 + }, + "values": [ + 0.10379690299809584, + 77.5886484375, + -7846.816694510807 + ] + }, + { + "number": 50, + "params": { + "support_angle": 27.800932022121827, + "tip_thickness": 58.466566117599996 + }, + "values": [ + 0.15936133358478716, + 89.5699765625, + -20932.418895641033 + ] + }, + { + "number": 51, + "params": { + "support_angle": 68.28160165372796, + "tip_thickness": 54.25192044349383 + }, + "values": [ + 0.1504814377690958, + 78.7045, + -19530.746021063354 + ] + }, + { + "number": 54, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14318368576930707, + 73.3545859375, + -17662.840771637857 + ] + }, + { + "number": 55, + "params": { + "support_angle": 42.007624686980066, + "tip_thickness": 33.66114704534336 + }, + "values": [ + 0.10792454729495918, + 76.6530859375, + -8932.199320124475 + ] + }, + { + "number": 56, + "params": { + "support_angle": 44.758845505563514, + "tip_thickness": 35.115723710618745 + }, + "values": [ + 0.11134663538881347, + 75.4997421875, + -9882.137428680566 + ] + }, + { + "number": 57, + "params": { + "support_angle": 21.71942605576092, + "tip_thickness": 36.370173320348286 + }, + "values": [ + 0.10885695256374972, + 66.265140625, + -8675.880501717687 + ] + }, + { + "number": 58, + "params": { + "support_angle": 65.4660201039391, + "tip_thickness": 37.763399448000506 + }, + "values": [ + 0.12474240279438661, + 70.5744921875, + -15099.03897523691 + ] + }, + { + "number": 59, + "params": { + "support_angle": 53.1261142176991, + "tip_thickness": 59.097295564859834 + }, + "values": [ + 0.16085535241637244, + 88.5342734375, + -21249.113101276806 + ] + }, + { + "number": 60, + "params": { + "support_angle": 35.58555380447055, + "tip_thickness": 38.736874205941255 + }, + "values": [ + 0.11601778105272115, + 72.884015625, + -10622.44850553897 + ] + }, + { + "number": 61, + "params": { + "support_angle": 46.00340105889054, + "tip_thickness": 33.66114704534336 + }, + "values": [ + 0.10928687283466162, + 75.3918984375, + -9490.685366079933 + ] + }, + { + "number": 63, + "params": { + "support_angle": 65.4660201039391, + "tip_thickness": 35.54563366576581 + }, + "values": [ + 0.12256021119520999, + 73.9141953125, + -14751.767038156198 + ] + }, + { + "number": 64, + "params": { + "support_angle": 65.4660201039391, + "tip_thickness": 33.66114704534336 + }, + "values": [ + 0.12088622396412017, + 73.7666328125, + -14534.6077771539 + ] + }, + { + "number": 66, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 56.844820512829465 + }, + "values": [ + 0.15580252944007203, + 86.998546875, + -20334.85014005634 + ] + }, + { + "number": 68, + "params": { + "support_angle": 42.007624686980066, + "tip_thickness": 33.66114704534336 + }, + "values": [ + 0.10792454729495918, + 76.6530859375, + -8932.199320124475 + ] + }, + { + "number": 69, + "params": { + "support_angle": 42.007624686980066, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.1428384621300621, + 89.5235703125, + -17175.03159590057 + ] + }, + { + "number": 70, + "params": { + "support_angle": 66.09371175115584, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14451915049322672, + 81.3326796875, + -18318.99156216132 + ] + }, + { + "number": 71, + "params": { + "support_angle": 24.424625102595975, + "tip_thickness": 37.763399448000506 + }, + "values": [ + 0.11225216497415444, + 73.2061484375, + -9512.275912549365 + ] + }, + { + "number": 72, + "params": { + "support_angle": 29.79914312095726, + "tip_thickness": 31.393512381599933 + }, + "values": [ + 0.1001870977541097, + 76.7433359375, + -6857.19564050316 + ] + }, + { + "number": 73, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14318368576930707, + 73.3545859375, + -17662.840771637857 + ] + }, + { + "number": 74, + "params": { + "support_angle": 65.4660201039391, + "tip_thickness": 31.35681866731614 + }, + "values": [ + 0.11917414734828162, + 76.8149140625, + -14293.737986512626 + ] + }, + { + "number": 75, + "params": { + "support_angle": 36.266516538163216, + "tip_thickness": 41.66031869068446 + }, + "values": [ + 0.12208318744805886, + 71.346875, + -12194.033598756263 + ] + }, + { + "number": 79, + "params": { + "support_angle": 27.046211248738132, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14244189663674706, + 92.3923984375, + -16924.103467261764 + ] + }, + { + "number": 81, + "params": { + "support_angle": 27.046211248738132, + "tip_thickness": 32.23651931039313 + }, + "values": [ + 0.10117670476504843, + 77.1213046875, + -6963.2759058274405 + ] + }, + { + "number": 82, + "params": { + "support_angle": 69.34434683002587, + "tip_thickness": 53.16734307889972 + }, + "values": [ + 0.1484814812990197, + 78.9971953125, + -19213.106474935073 + ] + }, + { + "number": 83, + "params": { + "support_angle": 29.93578407670862, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14253890471987404, + 92.747015625, + -16953.958902311737 + ] + }, + { + "number": 84, + "params": { + "support_angle": 65.4660201039391, + "tip_thickness": 37.763399448000506 + }, + "values": [ + 0.12474240279438661, + 70.5744921875, + -15099.03897523691 + ] + }, + { + "number": 85, + "params": { + "support_angle": 20.27610585618012, + "tip_thickness": 31.393512381599933 + }, + "values": [ + 0.0978146851246983, + 79.0321171875, + -6216.38192111036 + ] + }, + { + "number": 86, + "params": { + "support_angle": 60.77307142274171, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14392423954950373, + 81.0597109375, + -17967.08989727649 + ] + }, + { + "number": 87, + "params": { + "support_angle": 55.34286719238086, + "tip_thickness": 51.87021504122962 + }, + "values": [ + 0.1448005527358149, + 74.201671875, + -18129.786452030425 + ] + }, + { + "number": 90, + "params": { + "support_angle": 23.70223258670452, + "tip_thickness": 40.75397185632818 + }, + "values": [ + 0.11872281340810913, + 69.14565625, + -11115.394104203076 + ] + }, + { + "number": 91, + "params": { + "support_angle": 36.266516538163216, + "tip_thickness": 33.47607178575389 + }, + "values": [ + 0.10591993532641636, + 76.40140625, + -8238.445847536945 + ] + }, + { + "number": 92, + "params": { + "support_angle": 36.266516538163216, + "tip_thickness": 55.89310277626781 + }, + "values": [ + 0.15342884441980698, + 89.3893828125, + -19601.253058548966 + ] + }, + { + "number": 94, + "params": { + "support_angle": 23.177917514301182, + "tip_thickness": 58.466566117599996 + }, + "values": [ + 0.15936133358478716, + 89.5699765625, + -20932.418895641033 + ] + }, + { + "number": 96, + "params": { + "support_angle": 36.25916610133736, + "tip_thickness": 41.66031869068446 + }, + "values": [ + 0.12208171132496798, + 71.342453125, + -12193.66247694344 + ] + }, + { + "number": 97, + "params": { + "support_angle": 53.1261142176991, + "tip_thickness": 51.888185350141924 + }, + "values": [ + 0.14475850896684855, + 74.7981953125, + -18046.266694373393 + ] + }, + { + "number": 98, + "params": { + "support_angle": 51.87787356776066, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14330312401835696, + 74.7628671875, + -17696.812034510553 + ] + }, + { + "number": 102, + "params": { + "support_angle": 64.36063712881634, + "tip_thickness": 52.82355145850693 + }, + "values": [ + 0.14734292353877565, + 79.655328125, + -18806.274300781337 + ] + }, + { + "number": 104, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 56.844820512829465 + }, + "values": [ + 0.15580252944007203, + 86.998546875, + -20334.85014005634 + ] + }, + { + "number": 108, + "params": { + "support_angle": 41.37705091792748, + "tip_thickness": 30.762573802322855 + }, + "values": [ + 0.10266713153960678, + 77.295484375, + -7693.630790268831 + ] + }, + { + "number": 109, + "params": { + "support_angle": 69.34434683002587, + "tip_thickness": 33.23674280979913 + }, + "values": [ + 0.1253404407876966, + 64.0787578125, + -16418.654143000433 + ] + }, + { + "number": 110, + "params": { + "support_angle": 36.266516538163216, + "tip_thickness": 30.942875570602027 + }, + "values": [ + 0.10118614957762304, + 79.380640625, + -7200.350377920183 + ] + }, + { + "number": 111, + "params": { + "support_angle": 51.82052056318902, + "tip_thickness": 55.89310277626781 + }, + "values": [ + 0.15343496965411607, + 86.7441640625, + -19734.465057848753 + ] + }, + { + "number": 112, + "params": { + "support_angle": 35.717799053816336, + "tip_thickness": 31.35681866731614 + }, + "values": [ + 0.10177864447558861, + 78.81946875, + -7318.01369759942 + ] + }, + { + "number": 114, + "params": { + "support_angle": 32.464611457443745, + "tip_thickness": 42.31148769106889 + }, + "values": [ + 0.12308697031222822, + 70.5879296875, + -12331.585425697265 + ] + }, + { + "number": 115, + "params": { + "support_angle": 57.77755692715243, + "tip_thickness": 59.097295564859834 + }, + "values": [ + 0.16105139680347916, + 88.107265625, + -21237.079727106342 + ] + }, + { + "number": 116, + "params": { + "support_angle": 31.43990827458112, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.10240849008471971, + 76.4194375, + -7353.7859927675045 + ] + }, + { + "number": 117, + "params": { + "support_angle": 34.4875726456884, + "tip_thickness": 37.763399448000506 + }, + "values": [ + 0.11389330626904291, + 73.31809375, + -10066.41591951651 + ] + } + ] +} \ No newline at end of file diff --git a/studies/bracket_pareto_3obj/2_results/study.db b/studies/bracket_pareto_3obj/2_results/study.db new file mode 100644 index 00000000..f709e668 Binary files /dev/null and b/studies/bracket_pareto_3obj/2_results/study.db differ diff --git a/studies/bracket_pareto_3obj/2_results/surrogate_best.pt b/studies/bracket_pareto_3obj/2_results/surrogate_best.pt new file mode 100644 index 00000000..5a408994 Binary files /dev/null and b/studies/bracket_pareto_3obj/2_results/surrogate_best.pt differ diff --git a/studies/bracket_pareto_3obj/2_results/training_data.json b/studies/bracket_pareto_3obj/2_results/training_data.json new file mode 100644 index 00000000..6800ecba --- /dev/null +++ b/studies/bracket_pareto_3obj/2_results/training_data.json @@ -0,0 +1,2095 @@ +{ + "study_name": "bracket_pareto_3obj", + "n_trials": 117, + "timestamp": "2025-12-06T19:04:19.204971", + "trials": [ + { + "number": 0, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 1, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 47.959754525911094 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 2, + "params": { + "support_angle": 27.800932022121827, + "tip_thickness": 34.67983561008608 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 3, + "params": { + "support_angle": 22.904180608409973, + "tip_thickness": 55.98528437324806 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 4, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 5, + "params": { + "support_angle": 21.02922471479012, + "tip_thickness": 59.097295564859834 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 6, + "params": { + "support_angle": 61.622132040021086, + "tip_thickness": 36.370173320348286 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 7, + "params": { + "support_angle": 29.09124836035503, + "tip_thickness": 35.50213529560301 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 8, + "params": { + "support_angle": 35.21211214797688, + "tip_thickness": 45.74269294896713 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 9, + "params": { + "support_angle": 41.59725093210579, + "tip_thickness": 38.736874205941255 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 10, + "params": { + "support_angle": 50.59264473611897, + "tip_thickness": 34.18481581956125 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 11, + "params": { + "support_angle": 34.60723242676091, + "tip_thickness": 40.99085529881075 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 12, + "params": { + "support_angle": 42.8034992108518, + "tip_thickness": 53.55527884179041 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 14, + "params": { + "support_angle": 49.620728443102124, + "tip_thickness": 31.393512381599933 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 15, + "params": { + "support_angle": 50.37724259507192, + "tip_thickness": 35.115723710618745 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 16, + "params": { + "support_angle": 23.252579649263975, + "tip_thickness": 58.466566117599996 + }, + "values": [ + 0.10664433168238739, + 79.816609375, + -9339.578473236283 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10664433168238739, + "stiffness": -9339.578473236283, + "stress": 79.816609375 + } + }, + { + "number": 18, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.1594800904665372, + 89.4502578125, + -20960.59592691965 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1594800904665372, + "stiffness": -20960.59592691965, + "stress": 89.4502578125 + } + }, + { + "number": 19, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 47.959754525911094 + }, + "values": [ + 0.1370984916826118, + 80.2043046875, + -16381.0655256764 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1370984916826118, + "stiffness": -16381.0655256764, + "stress": 80.2043046875 + } + }, + { + "number": 20, + "params": { + "support_angle": 27.800932022121827, + "tip_thickness": 34.67983561008608 + }, + "values": [ + 0.10630918746092984, + 75.5471015625, + -8142.120566330409 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10630918746092984, + "stiffness": -8142.120566330409, + "stress": 75.5471015625 + } + }, + { + "number": 21, + "params": { + "support_angle": 22.904180608409973, + "tip_thickness": 55.98528437324806 + }, + "values": [ + 0.1535405869869207, + 89.3146875, + -19667.132759129887 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1535405869869207, + "stiffness": -19667.132759129887, + "stress": 89.3146875 + } + }, + { + "number": 22, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 36.370173320348286 + }, + "values": [ + 0.11780122136337835, + 72.6732421875, + -12366.875819243376 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11780122136337835, + "stiffness": -12366.875819243376, + "stress": 72.6732421875 + } + }, + { + "number": 23, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 35.115723710618745 + }, + "values": [ + 0.11313847406441968, + 75.261375, + -10673.210765604706 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11313847406441968, + "stiffness": -10673.210765604706, + "stress": 75.261375 + } + }, + { + "number": 24, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14318368576930707, + 73.3545859375, + -17662.840771637857 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14318368576930707, + "stiffness": -17662.840771637857, + "stress": 73.3545859375 + } + }, + { + "number": 25, + "params": { + "support_angle": 55.40362888980228, + "tip_thickness": 30.617534828874074 + }, + "values": [ + 0.10943751404355549, + 84.311, + -10332.376675469583 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10943751404355549, + "stiffness": -10332.376675469583, + "stress": 84.311 + } + }, + { + "number": 26, + "params": { + "support_angle": 50.59264473611897, + "tip_thickness": 59.097295564859834 + }, + "values": [ + 0.16085535241637244, + 88.5342734375, + -21249.113101276806 + ], + "user_attrs": { + "feasible": true, + "mass": 0.16085535241637244, + "stiffness": -21249.113101276806, + "stress": 88.5342734375 + } + }, + { + "number": 27, + "params": { + "support_angle": 61.622132040021086, + "tip_thickness": 36.370173320348286 + }, + "values": [ + 0.1205142172491795, + 80.8047578125, + -13545.316926624355 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1205142172491795, + "stiffness": -13545.316926624355, + "stress": 80.8047578125 + } + }, + { + "number": 28, + "params": { + "support_angle": 29.09124836035503, + "tip_thickness": 35.50213529560301 + }, + "values": [ + 0.10827058249925942, + 72.4169921875, + -8632.595914157022 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10827058249925942, + "stiffness": -8632.595914157022, + "stress": 72.4169921875 + } + }, + { + "number": 29, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 58.52142919229749 + }, + "values": [ + 0.1594800904665372, + 89.4502578125, + -20960.59592691965 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1594800904665372, + "stiffness": -20960.59592691965, + "stress": 89.4502578125 + } + }, + { + "number": 30, + "params": { + "support_angle": 35.21211214797688, + "tip_thickness": 45.74269294896713 + }, + "values": [ + 0.1306135502946057, + 73.6837265625, + -14277.746550071251 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1306135502946057, + "stiffness": -14277.746550071251, + "stress": 73.6837265625 + } + }, + { + "number": 31, + "params": { + "support_angle": 27.800932022121827, + "tip_thickness": 55.98528437324806 + }, + "values": [ + 0.1535405869869207, + 89.3146875, + -19667.132759129887 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1535405869869207, + "stiffness": -19667.132759129887, + "stress": 89.3146875 + } + }, + { + "number": 32, + "params": { + "support_angle": 41.59725093210579, + "tip_thickness": 45.74269294896713 + }, + "values": [ + 0.131096240070923, + 78.8313515625, + -14487.813655626978 + ], + "user_attrs": { + "feasible": true, + "mass": 0.131096240070923, + "stiffness": -14487.813655626978, + "stress": 78.8313515625 + } + }, + { + "number": 33, + "params": { + "support_angle": 29.09124836035503, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14251897231791152, + 92.731484375, + -16946.136144219086 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14251897231791152, + "stiffness": -16946.136144219086, + "stress": 92.731484375 + } + }, + { + "number": 34, + "params": { + "support_angle": 34.5614570099021, + "tip_thickness": 38.736874205941255 + }, + "values": [ + 0.11586110588460208, + 70.7065546875, + -10587.654595231894 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11586110588460208, + "stiffness": -10587.654595231894, + "stress": 70.7065546875 + } + }, + { + "number": 35, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 48.35558684167138 + }, + "values": [ + 0.13786499255161738, + 79.9088359375, + -16547.824880313816 + ], + "user_attrs": { + "feasible": true, + "mass": 0.13786499255161738, + "stiffness": -16547.824880313816, + "stress": 79.9088359375 + } + }, + { + "number": 36, + "params": { + "support_angle": 41.59725093210579, + "tip_thickness": 34.18481581956125 + }, + "values": [ + 0.10873759473302179, + 75.999984375, + -9102.751265315374 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10873759473302179, + "stiffness": -9102.751265315374, + "stress": 75.999984375 + } + }, + { + "number": 37, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 38.76433945605655 + }, + "values": [ + 0.12132943211029017, + 77.56953125, + -13069.619737933232 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12132943211029017, + "stiffness": -13069.619737933232, + "stress": 77.56953125 + } + }, + { + "number": 38, + "params": { + "support_angle": 38.318092164684586, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14271498947987588, + 89.380015625, + -17107.99153151556 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14271498947987588, + "stiffness": -17107.99153151556, + "stress": 89.380015625 + } + }, + { + "number": 39, + "params": { + "support_angle": 21.02922471479012, + "tip_thickness": 43.68209952651108 + }, + "values": [ + 0.12496673885629664, + 91.7453359375, + -12434.96390816167 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12496673885629664, + "stiffness": -12434.96390816167, + "stress": 91.7453359375 + } + }, + { + "number": 40, + "params": { + "support_angle": 50.59264473611897, + "tip_thickness": 34.18481581956125 + }, + "values": [ + 0.11190565081078178, + 76.7876328125, + -10422.469553635548 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11190565081078178, + "stiffness": -10422.469553635548, + "stress": 76.7876328125 + } + }, + { + "number": 41, + "params": { + "support_angle": 59.25879806965068, + "tip_thickness": 59.097295564859834 + }, + "values": [ + 0.16107048291123774, + 88.152171875, + -21236.738660830237 + ], + "user_attrs": { + "feasible": true, + "mass": 0.16107048291123774, + "stiffness": -21236.738660830237, + "stress": 88.152171875 + } + }, + { + "number": 42, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 35.990213464750795 + }, + "values": [ + 0.11728015145153212, + 81.8612734375, + -12173.339608456614 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11728015145153212, + "stiffness": -12173.339608456614, + "stress": 81.8612734375 + } + }, + { + "number": 43, + "params": { + "support_angle": 34.5614570099021, + "tip_thickness": 45.42703315240835 + }, + "values": [ + 0.12983421388432892, + 73.9091171875, + -14044.578216555325 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12983421388432892, + "stiffness": -14044.578216555325, + "stress": 73.9091171875 + } + }, + { + "number": 44, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 36.370173320348286 + }, + "values": [ + 0.11778844081157942, + 72.7532421875, + -12365.5598495581 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11778844081157942, + "stiffness": -12365.5598495581, + "stress": 72.7532421875 + } + }, + { + "number": 45, + "params": { + "support_angle": 50.59264473611897, + "tip_thickness": 47.772437065861276 + }, + "values": [ + 0.1360566289246654, + 83.084625, + -15877.636735478021 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1360566289246654, + "stiffness": -15877.636735478021, + "stress": 83.084625 + } + }, + { + "number": 46, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 31.393512381599933 + }, + "values": [ + 0.11118880708784562, + 84.8740390625, + -10878.054955936728 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11118880708784562, + "stiffness": -10878.054955936728, + "stress": 84.8740390625 + } + }, + { + "number": 47, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14318368576930707, + 73.3545859375, + -17662.840771637857 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14318368576930707, + "stiffness": -17662.840771637857, + "stress": 73.3545859375 + } + }, + { + "number": 48, + "params": { + "support_angle": 50.37724259507192, + "tip_thickness": 35.115723710618745 + }, + "values": [ + 0.11326420640721613, + 75.4833046875, + -10722.26724748861 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11326420640721613, + "stiffness": -10722.26724748861, + "stress": 75.4833046875 + } + }, + { + "number": 49, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 31.951547789558386 + }, + "values": [ + 0.10379690299809584, + 77.5886484375, + -7846.816694510807 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10379690299809584, + "stiffness": -7846.816694510807, + "stress": 77.5886484375 + } + }, + { + "number": 50, + "params": { + "support_angle": 27.800932022121827, + "tip_thickness": 58.466566117599996 + }, + "values": [ + 0.15936133358478716, + 89.5699765625, + -20932.418895641033 + ], + "user_attrs": { + "feasible": true, + "mass": 0.15936133358478716, + "stiffness": -20932.418895641033, + "stress": 89.5699765625 + } + }, + { + "number": 51, + "params": { + "support_angle": 68.28160165372796, + "tip_thickness": 54.25192044349383 + }, + "values": [ + 0.1504814377690958, + 78.7045, + -19530.746021063354 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1504814377690958, + "stiffness": -19530.746021063354, + "stress": 78.7045 + } + }, + { + "number": 52, + "params": { + "support_angle": 61.622132040021086, + "tip_thickness": 39.13841307520112 + }, + "values": [ + 0.12406792599571251, + 85.4307734375, + -14132.796624750525 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12406792599571251, + "stiffness": -14132.796624750525, + "stress": 85.4307734375 + } + }, + { + "number": 53, + "params": { + "support_angle": 24.883605700319194, + "tip_thickness": 50.52699079536471 + }, + "values": [ + 0.14083025141228153, + 95.9164609375, + -16462.994035406093 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14083025141228153, + "stiffness": -16462.994035406093, + "stress": 95.9164609375 + } + }, + { + "number": 54, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14318368576930707, + 73.3545859375, + -17662.840771637857 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14318368576930707, + "stiffness": -17662.840771637857, + "stress": 73.3545859375 + } + }, + { + "number": 55, + "params": { + "support_angle": 42.007624686980066, + "tip_thickness": 33.66114704534336 + }, + "values": [ + 0.10792454729495918, + 76.6530859375, + -8932.199320124475 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10792454729495918, + "stiffness": -8932.199320124475, + "stress": 76.6530859375 + } + }, + { + "number": 56, + "params": { + "support_angle": 44.758845505563514, + "tip_thickness": 35.115723710618745 + }, + "values": [ + 0.11134663538881347, + 75.4997421875, + -9882.137428680566 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11134663538881347, + "stiffness": -9882.137428680566, + "stress": 75.4997421875 + } + }, + { + "number": 57, + "params": { + "support_angle": 21.71942605576092, + "tip_thickness": 36.370173320348286 + }, + "values": [ + 0.10885695256374972, + 66.265140625, + -8675.880501717687 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10885695256374972, + "stiffness": -8675.880501717687, + "stress": 66.265140625 + } + }, + { + "number": 58, + "params": { + "support_angle": 65.4660201039391, + "tip_thickness": 37.763399448000506 + }, + "values": [ + 0.12474240279438661, + 70.5744921875, + -15099.03897523691 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12474240279438661, + "stiffness": -15099.03897523691, + "stress": 70.5744921875 + } + }, + { + "number": 59, + "params": { + "support_angle": 53.1261142176991, + "tip_thickness": 59.097295564859834 + }, + "values": [ + 0.16085535241637244, + 88.5342734375, + -21249.113101276806 + ], + "user_attrs": { + "feasible": true, + "mass": 0.16085535241637244, + "stiffness": -21249.113101276806, + "stress": 88.5342734375 + } + }, + { + "number": 60, + "params": { + "support_angle": 35.58555380447055, + "tip_thickness": 38.736874205941255 + }, + "values": [ + 0.11601778105272115, + 72.884015625, + -10622.44850553897 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11601778105272115, + "stiffness": -10622.44850553897, + "stress": 72.884015625 + } + }, + { + "number": 61, + "params": { + "support_angle": 46.00340105889054, + "tip_thickness": 33.66114704534336 + }, + "values": [ + 0.10928687283466162, + 75.3918984375, + -9490.685366079933 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10928687283466162, + "stiffness": -9490.685366079933, + "stress": 75.3918984375 + } + }, + { + "number": 62, + "params": { + "support_angle": 47.33551396716398, + "tip_thickness": 39.13841307520112 + }, + "values": [ + 0.11914905780316307, + 75.9223125, + -11862.685019522676 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11914905780316307, + "stiffness": -11862.685019522676, + "stress": 75.9223125 + } + }, + { + "number": 63, + "params": { + "support_angle": 65.4660201039391, + "tip_thickness": 35.54563366576581 + }, + "values": [ + 0.12256021119520999, + 73.9141953125, + -14751.767038156198 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12256021119520999, + "stiffness": -14751.767038156198, + "stress": 73.9141953125 + } + }, + { + "number": 64, + "params": { + "support_angle": 65.4660201039391, + "tip_thickness": 33.66114704534336 + }, + "values": [ + 0.12088622396412017, + 73.7666328125, + -14534.6077771539 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12088622396412017, + "stiffness": -14534.6077771539, + "stress": 73.7666328125 + } + }, + { + "number": 65, + "params": { + "support_angle": 68.47923138822793, + "tip_thickness": 53.253984700833435 + }, + "values": [ + 0.14854938557890152, + 79.5163203125, + -19177.40622690433 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14854938557890152, + "stiffness": -19177.40622690433, + "stress": 79.5163203125 + } + }, + { + "number": 66, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 56.844820512829465 + }, + "values": [ + 0.15580252944007203, + 86.998546875, + -20334.85014005634 + ], + "user_attrs": { + "feasible": true, + "mass": 0.15580252944007203, + "stiffness": -20334.85014005634, + "stress": 86.998546875 + } + }, + { + "number": 67, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 47.93699936433255 + }, + "values": [ + 0.13553002948553433, + 70.957171875, + -15622.418538451564 + ], + "user_attrs": { + "feasible": true, + "mass": 0.13553002948553433, + "stiffness": -15622.418538451564, + "stress": 70.957171875 + } + }, + { + "number": 68, + "params": { + "support_angle": 42.007624686980066, + "tip_thickness": 33.66114704534336 + }, + "values": [ + 0.10792454729495918, + 76.6530859375, + -8932.199320124475 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10792454729495918, + "stiffness": -8932.199320124475, + "stress": 76.6530859375 + } + }, + { + "number": 69, + "params": { + "support_angle": 42.007624686980066, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.1428384621300621, + 89.5235703125, + -17175.03159590057 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1428384621300621, + "stiffness": -17175.03159590057, + "stress": 89.5235703125 + } + }, + { + "number": 70, + "params": { + "support_angle": 66.09371175115584, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14451915049322672, + 81.3326796875, + -18318.99156216132 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14451915049322672, + "stiffness": -18318.99156216132, + "stress": 81.3326796875 + } + }, + { + "number": 71, + "params": { + "support_angle": 24.424625102595975, + "tip_thickness": 37.763399448000506 + }, + "values": [ + 0.11225216497415444, + 73.2061484375, + -9512.275912549365 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11225216497415444, + "stiffness": -9512.275912549365, + "stress": 73.2061484375 + } + }, + { + "number": 72, + "params": { + "support_angle": 29.79914312095726, + "tip_thickness": 31.393512381599933 + }, + "values": [ + 0.1001870977541097, + 76.7433359375, + -6857.19564050316 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1001870977541097, + "stiffness": -6857.19564050316, + "stress": 76.7433359375 + } + }, + { + "number": 73, + "params": { + "support_angle": 50.05575058716044, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14318368576930707, + 73.3545859375, + -17662.840771637857 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14318368576930707, + "stiffness": -17662.840771637857, + "stress": 73.3545859375 + } + }, + { + "number": 74, + "params": { + "support_angle": 65.4660201039391, + "tip_thickness": 31.35681866731614 + }, + "values": [ + 0.11917414734828162, + 76.8149140625, + -14293.737986512626 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11917414734828162, + "stiffness": -14293.737986512626, + "stress": 76.8149140625 + } + }, + { + "number": 75, + "params": { + "support_angle": 36.266516538163216, + "tip_thickness": 41.66031869068446 + }, + "values": [ + 0.12208318744805886, + 71.346875, + -12194.033598756263 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12208318744805886, + "stiffness": -12194.033598756263, + "stress": 71.346875 + } + }, + { + "number": 76, + "params": { + "support_angle": 33.567451588694794, + "tip_thickness": 47.772437065861276 + }, + "values": [ + 0.13489217381927426, + 69.8096875, + -15408.46297348648 + ], + "user_attrs": { + "feasible": true, + "mass": 0.13489217381927426, + "stiffness": -15408.46297348648, + "stress": 69.8096875 + } + }, + { + "number": 77, + "params": { + "support_angle": 61.436875457596464, + "tip_thickness": 40.702599800807675 + }, + "values": [ + 0.1261644237084926, + 86.3683359375, + -14493.445713611274 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1261644237084926, + "stiffness": -14493.445713611274, + "stress": 86.3683359375 + } + }, + { + "number": 78, + "params": { + "support_angle": 34.046725484369034, + "tip_thickness": 46.28088249474746 + }, + "values": [ + 0.13164077408543076, + 69.348734375, + -14551.68652044551 + ], + "user_attrs": { + "feasible": true, + "mass": 0.13164077408543076, + "stiffness": -14551.68652044551, + "stress": 69.348734375 + } + }, + { + "number": 79, + "params": { + "support_angle": 27.046211248738132, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14244189663674706, + 92.3923984375, + -16924.103467261764 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14244189663674706, + "stiffness": -16924.103467261764, + "stress": 92.3923984375 + } + }, + { + "number": 80, + "params": { + "support_angle": 56.59969709057025, + "tip_thickness": 54.06590942262119 + }, + "values": [ + 0.14948511075683624, + 92.751828125, + -18819.64239129604 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14948511075683624, + "stiffness": -18819.64239129604, + "stress": 92.751828125 + } + }, + { + "number": 81, + "params": { + "support_angle": 27.046211248738132, + "tip_thickness": 32.23651931039313 + }, + "values": [ + 0.10117670476504843, + 77.1213046875, + -6963.2759058274405 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10117670476504843, + "stiffness": -6963.2759058274405, + "stress": 77.1213046875 + } + }, + { + "number": 82, + "params": { + "support_angle": 69.34434683002587, + "tip_thickness": 53.16734307889972 + }, + "values": [ + 0.1484814812990197, + 78.9971953125, + -19213.106474935073 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1484814812990197, + "stiffness": -19213.106474935073, + "stress": 78.9971953125 + } + }, + { + "number": 83, + "params": { + "support_angle": 29.93578407670862, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14253890471987404, + 92.747015625, + -16953.958902311737 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14253890471987404, + "stiffness": -16953.958902311737, + "stress": 92.747015625 + } + }, + { + "number": 84, + "params": { + "support_angle": 65.4660201039391, + "tip_thickness": 37.763399448000506 + }, + "values": [ + 0.12474240279438661, + 70.5744921875, + -15099.03897523691 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12474240279438661, + "stiffness": -15099.03897523691, + "stress": 70.5744921875 + } + }, + { + "number": 85, + "params": { + "support_angle": 20.27610585618012, + "tip_thickness": 31.393512381599933 + }, + "values": [ + 0.0978146851246983, + 79.0321171875, + -6216.38192111036 + ], + "user_attrs": { + "feasible": true, + "mass": 0.0978146851246983, + "stiffness": -6216.38192111036, + "stress": 79.0321171875 + } + }, + { + "number": 86, + "params": { + "support_angle": 60.77307142274171, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14392423954950373, + 81.0597109375, + -17967.08989727649 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14392423954950373, + "stiffness": -17967.08989727649, + "stress": 81.0597109375 + } + }, + { + "number": 87, + "params": { + "support_angle": 55.34286719238086, + "tip_thickness": 51.87021504122962 + }, + "values": [ + 0.1448005527358149, + 74.201671875, + -18129.786452030425 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1448005527358149, + "stiffness": -18129.786452030425, + "stress": 74.201671875 + } + }, + { + "number": 88, + "params": { + "support_angle": 34.046725484369034, + "tip_thickness": 53.13811040057837 + }, + "values": [ + 0.1469193938426821, + 91.822625, + -18061.602518655192 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1469193938426821, + "stiffness": -18061.602518655192, + "stress": 91.822625 + } + }, + { + "number": 89, + "params": { + "support_angle": 34.046725484369034, + "tip_thickness": 46.28088249474746 + }, + "values": [ + 0.13164077408543076, + 69.348734375, + -14551.68652044551 + ], + "user_attrs": { + "feasible": true, + "mass": 0.13164077408543076, + "stiffness": -14551.68652044551, + "stress": 69.348734375 + } + }, + { + "number": 90, + "params": { + "support_angle": 23.70223258670452, + "tip_thickness": 40.75397185632818 + }, + "values": [ + 0.11872281340810913, + 69.14565625, + -11115.394104203076 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11872281340810913, + "stiffness": -11115.394104203076, + "stress": 69.14565625 + } + }, + { + "number": 91, + "params": { + "support_angle": 36.266516538163216, + "tip_thickness": 33.47607178575389 + }, + "values": [ + 0.10591993532641636, + 76.40140625, + -8238.445847536945 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10591993532641636, + "stiffness": -8238.445847536945, + "stress": 76.40140625 + } + }, + { + "number": 92, + "params": { + "support_angle": 36.266516538163216, + "tip_thickness": 55.89310277626781 + }, + "values": [ + 0.15342884441980698, + 89.3893828125, + -19601.253058548966 + ], + "user_attrs": { + "feasible": true, + "mass": 0.15342884441980698, + "stiffness": -19601.253058548966, + "stress": 89.3893828125 + } + }, + { + "number": 93, + "params": { + "support_angle": 51.1649063413779, + "tip_thickness": 39.92694074557947 + }, + "values": [ + 0.12150859666654186, + 79.405265625, + -12629.219797027632 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12150859666654186, + "stiffness": -12629.219797027632, + "stress": 79.405265625 + } + }, + { + "number": 94, + "params": { + "support_angle": 23.177917514301182, + "tip_thickness": 58.466566117599996 + }, + "values": [ + 0.15936133358478716, + 89.5699765625, + -20932.418895641033 + ], + "user_attrs": { + "feasible": true, + "mass": 0.15936133358478716, + "stiffness": -20932.418895641033, + "stress": 89.5699765625 + } + }, + { + "number": 95, + "params": { + "support_angle": 35.54911608578311, + "tip_thickness": 46.28088249474746 + }, + "values": [ + 0.1317916374856168, + 72.0955234375, + -14567.113560397618 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1317916374856168, + "stiffness": -14567.113560397618, + "stress": 72.0955234375 + } + }, + { + "number": 96, + "params": { + "support_angle": 36.25916610133736, + "tip_thickness": 41.66031869068446 + }, + "values": [ + 0.12208171132496798, + 71.342453125, + -12193.66247694344 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12208171132496798, + "stiffness": -12193.66247694344, + "stress": 71.342453125 + } + }, + { + "number": 97, + "params": { + "support_angle": 53.1261142176991, + "tip_thickness": 51.888185350141924 + }, + "values": [ + 0.14475850896684855, + 74.7981953125, + -18046.266694373393 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14475850896684855, + "stiffness": -18046.266694373393, + "stress": 74.7981953125 + } + }, + { + "number": 98, + "params": { + "support_angle": 51.87787356776066, + "tip_thickness": 51.242177333881365 + }, + "values": [ + 0.14330312401835696, + 74.7628671875, + -17696.812034510553 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14330312401835696, + "stiffness": -17696.812034510553, + "stress": 74.7628671875 + } + }, + { + "number": 99, + "params": { + "support_angle": 64.36063712881634, + "tip_thickness": 44.16644775485848 + }, + "values": [ + 0.1323440086103456, + 80.0266015625, + -16036.02491492292 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1323440086103456, + "stiffness": -16036.02491492292, + "stress": 80.0266015625 + } + }, + { + "number": 100, + "params": { + "support_angle": 25.979712296915086, + "tip_thickness": 51.39734361668985 + }, + "values": [ + 0.14277423024898006, + 92.3326328125, + -17005.40946392016 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14277423024898006, + "stiffness": -17005.40946392016, + "stress": 92.3326328125 + } + }, + { + "number": 101, + "params": { + "support_angle": 29.93578407670862, + "tip_thickness": 40.75397185632818 + }, + "values": [ + 0.1193650864493288, + 73.079265625, + -11307.177888035814 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1193650864493288, + "stiffness": -11307.177888035814, + "stress": 73.079265625 + } + }, + { + "number": 102, + "params": { + "support_angle": 64.36063712881634, + "tip_thickness": 52.82355145850693 + }, + "values": [ + 0.14734292353877565, + 79.655328125, + -18806.274300781337 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14734292353877565, + "stiffness": -18806.274300781337, + "stress": 79.655328125 + } + }, + { + "number": 103, + "params": { + "support_angle": 38.72700594236812, + "tip_thickness": 46.83831592708489 + }, + "values": [ + 0.13319852056789458, + 72.0601484375, + -15015.23957386964 + ], + "user_attrs": { + "feasible": true, + "mass": 0.13319852056789458, + "stiffness": -15015.23957386964, + "stress": 72.0601484375 + } + }, + { + "number": 104, + "params": { + "support_angle": 66.97494707820945, + "tip_thickness": 56.844820512829465 + }, + "values": [ + 0.15580252944007203, + 86.998546875, + -20334.85014005634 + ], + "user_attrs": { + "feasible": true, + "mass": 0.15580252944007203, + "stiffness": -20334.85014005634, + "stress": 86.998546875 + } + }, + { + "number": 105, + "params": { + "support_angle": 20.27610585618012, + "tip_thickness": 53.12901539863683 + }, + "values": [ + 0.14685299789123613, + 91.4424765625, + -17980.843553228624 + ], + "user_attrs": { + "feasible": true, + "mass": 0.14685299789123613, + "stiffness": -17980.843553228624, + "stress": 91.4424765625 + } + }, + { + "number": 106, + "params": { + "support_angle": 36.266516538163216, + "tip_thickness": 44.81386789093172 + }, + "values": [ + 0.12865452055089624, + 78.9740390625, + -13746.868365925257 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12865452055089624, + "stiffness": -13746.868365925257, + "stress": 78.9740390625 + } + }, + { + "number": 107, + "params": { + "support_angle": 36.266516538163216, + "tip_thickness": 45.681984881459826 + }, + "values": [ + 0.13055558421682126, + 70.4417890625, + -14316.046093286266 + ], + "user_attrs": { + "feasible": true, + "mass": 0.13055558421682126, + "stiffness": -14316.046093286266, + "stress": 70.4417890625 + } + }, + { + "number": 108, + "params": { + "support_angle": 41.37705091792748, + "tip_thickness": 30.762573802322855 + }, + "values": [ + 0.10266713153960678, + 77.295484375, + -7693.630790268831 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10266713153960678, + "stiffness": -7693.630790268831, + "stress": 77.295484375 + } + }, + { + "number": 109, + "params": { + "support_angle": 69.34434683002587, + "tip_thickness": 33.23674280979913 + }, + "values": [ + 0.1253404407876966, + 64.0787578125, + -16418.654143000433 + ], + "user_attrs": { + "feasible": true, + "mass": 0.1253404407876966, + "stiffness": -16418.654143000433, + "stress": 64.0787578125 + } + }, + { + "number": 110, + "params": { + "support_angle": 36.266516538163216, + "tip_thickness": 30.942875570602027 + }, + "values": [ + 0.10118614957762304, + 79.380640625, + -7200.350377920183 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10118614957762304, + "stiffness": -7200.350377920183, + "stress": 79.380640625 + } + }, + { + "number": 111, + "params": { + "support_angle": 51.82052056318902, + "tip_thickness": 55.89310277626781 + }, + "values": [ + 0.15343496965411607, + 86.7441640625, + -19734.465057848753 + ], + "user_attrs": { + "feasible": true, + "mass": 0.15343496965411607, + "stiffness": -19734.465057848753, + "stress": 86.7441640625 + } + }, + { + "number": 112, + "params": { + "support_angle": 35.717799053816336, + "tip_thickness": 31.35681866731614 + }, + "values": [ + 0.10177864447558861, + 78.81946875, + -7318.01369759942 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10177864447558861, + "stiffness": -7318.01369759942, + "stress": 78.81946875 + } + }, + { + "number": 113, + "params": { + "support_angle": 45.42853455823514, + "tip_thickness": 57.22699421778279 + }, + "values": [ + 0.15654635041930973, + 90.157671875, + -20302.25459128466 + ], + "user_attrs": { + "feasible": true, + "mass": 0.15654635041930973, + "stiffness": -20302.25459128466, + "stress": 90.157671875 + } + }, + { + "number": 114, + "params": { + "support_angle": 32.464611457443745, + "tip_thickness": 42.31148769106889 + }, + "values": [ + 0.12308697031222822, + 70.5879296875, + -12331.585425697265 + ], + "user_attrs": { + "feasible": true, + "mass": 0.12308697031222822, + "stiffness": -12331.585425697265, + "stress": 70.5879296875 + } + }, + { + "number": 115, + "params": { + "support_angle": 57.77755692715243, + "tip_thickness": 59.097295564859834 + }, + "values": [ + 0.16105139680347916, + 88.107265625, + -21237.079727106342 + ], + "user_attrs": { + "feasible": true, + "mass": 0.16105139680347916, + "stiffness": -21237.079727106342, + "stress": 88.107265625 + } + }, + { + "number": 116, + "params": { + "support_angle": 31.43990827458112, + "tip_thickness": 32.30939729486379 + }, + "values": [ + 0.10240849008471971, + 76.4194375, + -7353.7859927675045 + ], + "user_attrs": { + "feasible": true, + "mass": 0.10240849008471971, + "stiffness": -7353.7859927675045, + "stress": 76.4194375 + } + }, + { + "number": 117, + "params": { + "support_angle": 34.4875726456884, + "tip_thickness": 37.763399448000506 + }, + "values": [ + 0.11389330626904291, + 73.31809375, + -10066.41591951651 + ], + "user_attrs": { + "feasible": true, + "mass": 0.11389330626904291, + "stiffness": -10066.41591951651, + "stress": 73.31809375 + } + } + ] +} \ No newline at end of file diff --git a/studies/bracket_pareto_3obj/2_results/turbo_report.json b/studies/bracket_pareto_3obj/2_results/turbo_report.json new file mode 100644 index 00000000..482d31bc --- /dev/null +++ b/studies/bracket_pareto_3obj/2_results/turbo_report.json @@ -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 + ] + } + ] +} \ No newline at end of file diff --git a/studies/bracket_pareto_3obj/2_results/validation_report.json b/studies/bracket_pareto_3obj/2_results/validation_report.json new file mode 100644 index 00000000..3b96ff78 --- /dev/null +++ b/studies/bracket_pareto_3obj/2_results/validation_report.json @@ -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 + ] + } + ] +} \ No newline at end of file diff --git a/studies/bracket_pareto_3obj/MODEL_INTROSPECTION.md b/studies/bracket_pareto_3obj/MODEL_INTROSPECTION.md new file mode 100644 index 00000000..278af866 --- /dev/null +++ b/studies/bracket_pareto_3obj/MODEL_INTROSPECTION.md @@ -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.* diff --git a/studies/bracket_pareto_3obj/README.md b/studies/bracket_pareto_3obj/README.md new file mode 100644 index 00000000..d0ada651 --- /dev/null +++ b/studies/bracket_pareto_3obj/README.md @@ -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) diff --git a/studies/bracket_pareto_3obj/STUDY_REPORT.md b/studies/bracket_pareto_3obj/STUDY_REPORT.md new file mode 100644 index 00000000..ff4cee10 --- /dev/null +++ b/studies/bracket_pareto_3obj/STUDY_REPORT.md @@ -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* diff --git a/studies/bracket_pareto_3obj/reset_study.py b/studies/bracket_pareto_3obj/reset_study.py new file mode 100644 index 00000000..2eed91f2 --- /dev/null +++ b/studies/bracket_pareto_3obj/reset_study.py @@ -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() diff --git a/studies/bracket_pareto_3obj/run_nn_optimization.py b/studies/bracket_pareto_3obj/run_nn_optimization.py new file mode 100644 index 00000000..1cb6112f --- /dev/null +++ b/studies/bracket_pareto_3obj/run_nn_optimization.py @@ -0,0 +1,1095 @@ +""" +bracket_pareto_3obj - Hybrid Neural Network Optimization Script +================================================================ + +This script implements the hybrid NN/FEA optimization workflow: + +Phase 1: Export - Extract training data from existing FEA trials +Phase 2: Train - Train MLP surrogate model on FEA results +Phase 3: NN-Optimize - Run fast NN-only optimization (1000s of trials) +Phase 4: Validate - Validate best NN predictions with actual FEA + +Workflow: +--------- +1. python run_nn_optimization.py --export # Export training data +2. python run_nn_optimization.py --train # Train surrogate model +3. python run_nn_optimization.py --nn-optimize # Run NN optimization +4. python run_nn_optimization.py --validate # Validate with FEA + +Or run all phases: + python run_nn_optimization.py --all + +Generated for bracket_pareto_3obj study +""" + +from pathlib import Path +import sys +import json +import argparse +from datetime import datetime +from typing import Dict, Any, Optional, List, Tuple +import numpy as np + +# 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, TPESampler + +# 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 + +# Neural surrogate imports +try: + import torch + import torch.nn as nn + import torch.nn.functional as F + from torch.utils.data import Dataset, DataLoader, random_split + TORCH_AVAILABLE = True +except ImportError: + TORCH_AVAILABLE = False + print("WARNING: PyTorch not available. NN features disabled.") + + +# ============================================================================ +# MLP Surrogate Model +# ============================================================================ + +class MLPSurrogate(nn.Module): + """Simple MLP for design parameters -> objectives prediction.""" + + def __init__(self, n_inputs: int = 2, n_outputs: int = 3, + hidden_dims: List[int] = [64, 128, 64]): + super().__init__() + + layers = [] + prev_dim = n_inputs + + for hidden_dim in hidden_dims: + layers.extend([ + nn.Linear(prev_dim, hidden_dim), + nn.LayerNorm(hidden_dim), + nn.ReLU(), + nn.Dropout(0.1) + ]) + prev_dim = hidden_dim + + layers.append(nn.Linear(prev_dim, n_outputs)) + self.network = nn.Sequential(*layers) + + # Initialize weights + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight) + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, x): + return self.network(x) + + +class BracketSurrogate: + """Surrogate model for bracket Pareto optimization.""" + + def __init__(self, model_path: Path = None, device: str = 'auto'): + if not TORCH_AVAILABLE: + raise ImportError("PyTorch required") + + self.device = torch.device('cuda' if torch.cuda.is_available() and device == 'auto' else 'cpu') + self.model = None + self.normalization = None + self.design_var_names = ['support_angle', 'tip_thickness'] + self.objective_names = ['mass', 'stress', 'stiffness'] + + if model_path and Path(model_path).exists(): + self.load(model_path) + + def train_from_database(self, db_path: Path, study_name: str, + epochs: int = 300, save_path: Path = None): + """Train surrogate from Optuna database.""" + + print(f"\n{'='*60}") + print("Training Bracket Surrogate Model") + print(f"{'='*60}") + print(f"Device: {self.device}") + print(f"Database: {db_path}") + + # Load data from database + storage = optuna.storages.RDBStorage(f"sqlite:///{db_path}") + study = optuna.load_study(study_name=study_name, storage=storage) + + completed = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE] + print(f"Found {len(completed)} completed trials") + + if len(completed) < 10: + raise ValueError(f"Need at least 10 trials, got {len(completed)}") + + # Extract data + design_params = [] + objectives = [] + + for trial in completed: + # Skip trials with infinite values + if any(v == float('inf') for v in trial.values): + continue + + # Design parameters + params = [ + trial.params.get('support_angle', 45.0), + trial.params.get('tip_thickness', 45.0) + ] + + # Objectives: mass, stress, stiffness (all 3 values from multi-objective) + objs = list(trial.values) # [mass, stress, stiffness] + + design_params.append(params) + objectives.append(objs) + + design_params = np.array(design_params, dtype=np.float32) + objectives = np.array(objectives, dtype=np.float32) + + print(f"Valid samples: {len(design_params)}") + print(f"Design var ranges:") + print(f" support_angle: {design_params[:, 0].min():.1f} - {design_params[:, 0].max():.1f}") + print(f" tip_thickness: {design_params[:, 1].min():.1f} - {design_params[:, 1].max():.1f}") + print(f"Objective ranges:") + for i, name in enumerate(self.objective_names): + print(f" {name}: {objectives[:, i].min():.4f} - {objectives[:, i].max():.4f}") + + # Compute normalization stats + design_mean = design_params.mean(axis=0) + design_std = design_params.std(axis=0) + 1e-8 + objective_mean = objectives.mean(axis=0) + objective_std = objectives.std(axis=0) + 1e-8 + + self.normalization = { + 'design_mean': design_mean, + 'design_std': design_std, + 'objective_mean': objective_mean, + 'objective_std': objective_std + } + + # Normalize + X = (design_params - design_mean) / design_std + Y = (objectives - objective_mean) / objective_std + + # Create dataset + X_tensor = torch.tensor(X, dtype=torch.float32) + Y_tensor = torch.tensor(Y, dtype=torch.float32) + + dataset = torch.utils.data.TensorDataset(X_tensor, Y_tensor) + + n_val = max(1, int(len(dataset) * 0.2)) + n_train = len(dataset) - n_val + train_ds, val_ds = random_split(dataset, [n_train, n_val]) + + train_loader = DataLoader(train_ds, batch_size=16, shuffle=True) + val_loader = DataLoader(val_ds, batch_size=16) + + print(f"\nTraining: {n_train} samples, Validation: {n_val} samples") + + # Create model + self.model = MLPSurrogate( + n_inputs=len(self.design_var_names), + n_outputs=len(self.objective_names), + hidden_dims=[64, 128, 128, 64] + ).to(self.device) + + n_params = sum(p.numel() for p in self.model.parameters()) + print(f"Model parameters: {n_params:,}") + + # Training + optimizer = torch.optim.AdamW(self.model.parameters(), lr=0.001, weight_decay=1e-5) + scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, epochs) + + best_val_loss = float('inf') + best_state = None + + print(f"\nTraining for {epochs} epochs...") + + for epoch in range(epochs): + # Train + self.model.train() + train_loss = 0.0 + for x, y in train_loader: + x, y = x.to(self.device), y.to(self.device) + optimizer.zero_grad() + pred = self.model(x) + loss = F.mse_loss(pred, y) + loss.backward() + optimizer.step() + train_loss += loss.item() + train_loss /= len(train_loader) + + # Validate + self.model.eval() + val_loss = 0.0 + with torch.no_grad(): + for x, y in val_loader: + x, y = x.to(self.device), y.to(self.device) + pred = self.model(x) + val_loss += F.mse_loss(pred, y).item() + val_loss /= len(val_loader) + + scheduler.step() + + if val_loss < best_val_loss: + best_val_loss = val_loss + best_state = self.model.state_dict().copy() + + if (epoch + 1) % 50 == 0 or epoch == 0: + print(f" Epoch {epoch+1:3d}: train={train_loss:.6f}, val={val_loss:.6f}") + + # Load best model + self.model.load_state_dict(best_state) + print(f"\nBest validation loss: {best_val_loss:.6f}") + + # Evaluate accuracy + self.model.eval() + all_preds = [] + all_targets = [] + + with torch.no_grad(): + for x, y in val_loader: + x = x.to(self.device) + pred = self.model(x).cpu().numpy() + all_preds.append(pred) + all_targets.append(y.numpy()) + + all_preds = np.concatenate(all_preds) + all_targets = np.concatenate(all_targets) + + # Denormalize + preds_denorm = all_preds * objective_std + objective_mean + targets_denorm = all_targets * objective_std + objective_mean + + print(f"\nValidation accuracy:") + for i, name in enumerate(self.objective_names): + mae = np.abs(preds_denorm[:, i] - targets_denorm[:, i]).mean() + mape = (np.abs(preds_denorm[:, i] - targets_denorm[:, i]) / + (np.abs(targets_denorm[:, i]) + 1e-8)).mean() * 100 + print(f" {name}: MAE={mae:.4f}, MAPE={mape:.1f}%") + + # Save if requested + if save_path: + self.save(save_path) + + print(f"\n{'='*60}") + print("Training complete!") + print(f"{'='*60}\n") + + return self + + def predict(self, design_params: Dict[str, float]) -> Dict[str, float]: + """Predict objectives from design parameters.""" + if self.model is None: + raise ValueError("Model not trained") + + # Build input + x = np.array([ + design_params.get('support_angle', 45.0), + design_params.get('tip_thickness', 45.0) + ], dtype=np.float32) + + # Normalize + x_norm = (x - self.normalization['design_mean']) / self.normalization['design_std'] + x_tensor = torch.tensor(x_norm, dtype=torch.float32, device=self.device).unsqueeze(0) + + # Predict + self.model.eval() + with torch.no_grad(): + y_norm = self.model(x_tensor).cpu().numpy()[0] + + # Denormalize + y = y_norm * self.normalization['objective_std'] + self.normalization['objective_mean'] + + return { + 'mass': float(y[0]), + 'stress': float(y[1]), + 'stiffness': float(y[2]) + } + + def save(self, path: Path): + """Save model to file.""" + torch.save({ + 'model_state_dict': self.model.state_dict(), + 'normalization': { + 'design_mean': self.normalization['design_mean'].tolist(), + 'design_std': self.normalization['design_std'].tolist(), + 'objective_mean': self.normalization['objective_mean'].tolist(), + 'objective_std': self.normalization['objective_std'].tolist() + }, + 'design_var_names': self.design_var_names, + 'objective_names': self.objective_names + }, path) + print(f"Model saved to {path}") + + def load(self, path: Path): + """Load model from file.""" + checkpoint = torch.load(path, map_location=self.device) + + self.model = MLPSurrogate( + n_inputs=2, n_outputs=3, hidden_dims=[64, 128, 128, 64] + ).to(self.device) + self.model.load_state_dict(checkpoint['model_state_dict']) + self.model.eval() + + norm = checkpoint['normalization'] + self.normalization = { + 'design_mean': np.array(norm['design_mean']), + 'design_std': np.array(norm['design_std']), + 'objective_mean': np.array(norm['objective_mean']), + 'objective_std': np.array(norm['objective_std']) + } + + self.design_var_names = checkpoint.get('design_var_names', ['support_angle', 'tip_thickness']) + self.objective_names = checkpoint.get('objective_names', ['mass', 'stress', 'stiffness']) + + print(f"Model loaded from {path}") + + +# ============================================================================ +# Phase Functions +# ============================================================================ + +def phase_export(results_dir: Path, study_name: str, logger): + """Phase 1: Export training data summary from database.""" + + print(f"\n{'='*60}") + print("PHASE 1: Export Training Data Summary") + print(f"{'='*60}") + + db_path = results_dir / "study.db" + if not db_path.exists(): + print(f"ERROR: Database not found at {db_path}") + return False + + storage = optuna.storages.RDBStorage(f"sqlite:///{db_path}") + study = optuna.load_study(study_name=study_name, storage=storage) + + completed = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE] + print(f"Completed trials: {len(completed)}") + + # Export summary + export_data = { + 'study_name': study_name, + 'n_trials': len(completed), + 'timestamp': datetime.now().isoformat(), + 'trials': [] + } + + for trial in completed: + if any(v == float('inf') for v in trial.values): + continue + + export_data['trials'].append({ + 'number': trial.number, + 'params': trial.params, + 'values': trial.values, + 'user_attrs': trial.user_attrs + }) + + export_path = results_dir / "training_data.json" + with open(export_path, 'w') as f: + json.dump(export_data, f, indent=2) + + print(f"Exported {len(export_data['trials'])} valid trials to {export_path}") + + # Also save Pareto front + pareto_trials = study.best_trials + pareto_data = { + 'n_pareto': len(pareto_trials), + 'trials': [{ + 'number': t.number, + 'params': t.params, + 'values': t.values + } for t in pareto_trials] + } + + pareto_path = results_dir / "pareto_front.json" + with open(pareto_path, 'w') as f: + json.dump(pareto_data, f, indent=2) + + print(f"Pareto front: {len(pareto_trials)} solutions saved to {pareto_path}") + + return True + + +def phase_train(results_dir: Path, study_name: str, logger, epochs: int = 300): + """Phase 2: Train surrogate model.""" + + print(f"\n{'='*60}") + print("PHASE 2: Train Surrogate Model") + print(f"{'='*60}") + + if not TORCH_AVAILABLE: + print("ERROR: PyTorch not available") + return None + + db_path = results_dir / "study.db" + model_path = results_dir / "surrogate_best.pt" + + surrogate = BracketSurrogate(device='auto') + surrogate.train_from_database( + db_path=db_path, + study_name=study_name, + epochs=epochs, + save_path=model_path + ) + + return surrogate + + +def phase_nn_optimize(results_dir: Path, study_name: str, surrogate: BracketSurrogate, + n_trials: int = 1000, logger=None): + """Phase 3: Run NN-accelerated optimization. + + NOTE: NN results are stored in a SEPARATE database (nn_study.db) to avoid + overloading the dashboard. Only FEA-validated results go into the main study.db. + """ + + print(f"\n{'='*60}") + print("PHASE 3: Neural Network Optimization") + print(f"{'='*60}") + print(f"Running {n_trials} trials using NN surrogate (~milliseconds each)") + print(f"NOTE: NN results stored in separate nn_study.db (not shown in dashboard)") + + # Create NN-only study in SEPARATE database + # This prevents the dashboard from being overloaded with 1000s of NN trials + nn_db_path = results_dir / "nn_study.db" + storage = f"sqlite:///{nn_db_path}" + + # Use NSGA-II for multi-objective + nn_study = optuna.create_study( + study_name=f"{study_name}_nn", + storage=storage, + sampler=NSGAIISampler(population_size=50, seed=42), + directions=['minimize', 'minimize', 'minimize'], # mass, stress, -stiffness + load_if_exists=True + ) + + # Define NN objective + def nn_objective(trial): + support_angle = trial.suggest_float('support_angle', 20.0, 70.0) + tip_thickness = trial.suggest_float('tip_thickness', 30.0, 60.0) + + # NN prediction (sub-millisecond) + pred = surrogate.predict({ + 'support_angle': support_angle, + 'tip_thickness': tip_thickness + }) + + # Store for later analysis + trial.set_user_attr('nn_mass', pred['mass']) + trial.set_user_attr('nn_stress', pred['stress']) + trial.set_user_attr('nn_stiffness', pred['stiffness']) + + return pred['mass'], pred['stress'], pred['stiffness'] + + # Run NN optimization + import time + start = time.time() + + nn_study.optimize(nn_objective, n_trials=n_trials, show_progress_bar=True) + + elapsed = time.time() - start + print(f"\nNN optimization completed:") + print(f" Trials: {n_trials}") + print(f" Time: {elapsed:.1f}s ({elapsed/n_trials*1000:.2f}ms per trial)") + + # Get Pareto front + pareto_trials = nn_study.best_trials + print(f" Pareto solutions: {len(pareto_trials)}") + + # Save NN Pareto front + nn_pareto = { + 'type': 'nn_optimization', + 'n_trials': n_trials, + 'time_seconds': elapsed, + 'n_pareto': len(pareto_trials), + 'trials': [{ + 'number': t.number, + 'params': t.params, + 'values': t.values + } for t in pareto_trials] + } + + nn_pareto_path = results_dir / "nn_pareto_front.json" + with open(nn_pareto_path, 'w') as f: + json.dump(nn_pareto, f, indent=2) + + print(f" NN Pareto saved to {nn_pareto_path}") + + # Save optimization state + state = { + 'phase': 'nn_optimization', + 'timestamp': datetime.now().isoformat(), + 'n_trials': n_trials, + 'n_pareto': len(pareto_trials), + 'best_candidates': [{ + 'params': t.params, + 'nn_objectives': t.values + } for t in pareto_trials[:20]] # Top 20 candidates for validation + } + + state_path = results_dir / "nn_optimization_state.json" + with open(state_path, 'w') as f: + json.dump(state, f, indent=2) + + return nn_study, pareto_trials + + +def phase_validate(results_dir: Path, model_dir: Path, config: dict, + surrogate: BracketSurrogate, n_validate: int = 10, logger=None, + add_to_main_study: bool = True, study_name: str = "bracket_pareto_3obj"): + """Phase 4: Validate best NN predictions with FEA. + + Validated results are added to the main study.db so they: + 1. Appear in the dashboard alongside the original FEA runs + 2. Are included in future surrogate retraining + """ + + print(f"\n{'='*60}") + print("PHASE 4: FEA Validation of NN Predictions") + print(f"{'='*60}") + if add_to_main_study: + print(f"NOTE: Validated results will be added to main study.db (visible in dashboard)") + + # Load NN Pareto front + nn_pareto_path = results_dir / "nn_pareto_front.json" + if not nn_pareto_path.exists(): + print("ERROR: NN Pareto front not found. Run --nn-optimize first.") + return None + + with open(nn_pareto_path) as f: + nn_pareto = json.load(f) + + candidates = nn_pareto['trials'][:n_validate] + print(f"Validating {len(candidates)} best NN candidates with FEA") + + # Initialize NX solver + nx_solver = NXSolver(nastran_version="2506") + sim_file = model_dir / config['simulation']['sim_file'] + + validated_results = [] + + for i, candidate in enumerate(candidates): + params = candidate['params'] + nn_values = candidate['values'] + + print(f"\n Validating candidate {i+1}/{len(candidates)}:") + print(f" support_angle={params['support_angle']:.1f}, tip_thickness={params['tip_thickness']:.1f}") + print(f" NN prediction: mass={nn_values[0]:.4f}, stress={nn_values[1]:.2f}, stiffness={nn_values[2]:.2f}") + + # Run FEA + result = nx_solver.run_simulation( + sim_file=sim_file, + working_dir=model_dir, + expression_updates=params, + solution_name=config['simulation'].get('solution_name'), + cleanup=True + ) + + if not result['success']: + print(f" FEA FAILED: {result.get('error', 'Unknown')}") + continue + + # Extract results + op2_file = result['op2_file'] + dat_file = model_dir / config['simulation']['dat_file'] + + fea_mass = extract_mass_from_bdf(str(dat_file)) + stress_result = extract_solid_stress(op2_file, subcase=1, element_type='chexa') + fea_stress = stress_result.get('max_von_mises', float('inf')) / 1000.0 + disp_result = extract_displacement(op2_file, subcase=1) + max_disp = disp_result['max_displacement'] + fea_stiffness = -1000.0 / max(abs(max_disp), 1e-6) + + print(f" FEA result: mass={fea_mass:.4f}, stress={fea_stress:.2f}, stiffness={fea_stiffness:.2f}") + + # Compute errors + mass_err = abs(fea_mass - nn_values[0]) / fea_mass * 100 + stress_err = abs(fea_stress - nn_values[1]) / fea_stress * 100 + stiff_err = abs(fea_stiffness - nn_values[2]) / abs(fea_stiffness) * 100 + + print(f" NN Error: mass={mass_err:.1f}%, stress={stress_err:.1f}%, stiffness={stiff_err:.1f}%") + + validated_results.append({ + 'params': params, + 'nn_objectives': nn_values, + 'fea_objectives': [fea_mass, fea_stress, fea_stiffness], + 'errors_percent': [mass_err, stress_err, stiff_err] + }) + + # Add validated results to main study database + if add_to_main_study and validated_results: + print(f"\nAdding {len(validated_results)} validated results to main study.db...") + main_db_path = results_dir / "study.db" + main_storage = f"sqlite:///{main_db_path}" + + try: + # Load existing study + main_study = optuna.load_study( + study_name=study_name, + storage=main_storage, + sampler=NSGAIISampler(population_size=20, seed=42) + ) + + # Add each validated result as a new trial + for result in validated_results: + # Create a new trial with the FEA results + trial = main_study.ask() + + # Set the parameters + for param_name, param_value in result['params'].items(): + trial.suggest_float(param_name, param_value, param_value) + + # Tell the study the FEA objective values + fea_objs = result['fea_objectives'] + main_study.tell(trial, fea_objs) + + # Mark as NN-validated + trial.set_user_attr('source', 'nn_validated') + trial.set_user_attr('nn_prediction', result['nn_objectives']) + trial.set_user_attr('nn_error_percent', result['errors_percent']) + + print(f" Added {len(validated_results)} trials to main study (now {len(main_study.trials)} total)") + + except Exception as e: + print(f" WARNING: Could not add to main study: {e}") + + # Summary + print(f"\n{'='*60}") + print("Validation Summary") + print(f"{'='*60}") + + if validated_results: + avg_errors = np.array([r['errors_percent'] for r in validated_results]).mean(axis=0) + print(f"Average NN prediction error:") + print(f" Mass: {avg_errors[0]:.1f}%") + print(f" Stress: {avg_errors[1]:.1f}%") + print(f" Stiffness: {avg_errors[2]:.1f}%") + + # Save validation results + validation_report = { + 'timestamp': datetime.now().isoformat(), + 'n_validated': len(validated_results), + 'average_errors_percent': { + 'mass': float(avg_errors[0]), + 'stress': float(avg_errors[1]), + 'stiffness': float(avg_errors[2]) + }, + 'results': validated_results + } + + report_path = results_dir / "validation_report.json" + with open(report_path, 'w') as f: + json.dump(validation_report, f, indent=2) + + print(f"\nValidation report saved to {report_path}") + + return validated_results + + +def phase_hybrid_loop(results_dir: Path, model_dir: Path, config: dict, + study_name: str, n_iterations: int = 3, + nn_trials_per_iter: int = 500, validate_per_iter: int = 5, + epochs: int = 300, logger=None): + """ + Run adaptive hybrid loop: Train → NN-Optimize → Validate → Retrain → Repeat + + This continuously improves the surrogate by: + 1. Running NN optimization to find promising candidates + 2. Validating top candidates with FEA + 3. Adding FEA results to training data + 4. Retraining surrogate with expanded dataset + 5. Repeat until convergence or max iterations + + Args: + n_iterations: Number of hybrid loop iterations + nn_trials_per_iter: NN trials per iteration + validate_per_iter: FEA validations per iteration + """ + + print(f"\n{'#'*60}") + print("# HYBRID ADAPTIVE LOOP") + print(f"{'#'*60}") + print(f"Iterations: {n_iterations}") + print(f"NN trials per iteration: {nn_trials_per_iter}") + print(f"FEA validations per iteration: {validate_per_iter}") + print(f"Total FEA budget: {n_iterations * validate_per_iter} additional runs") + + model_path = results_dir / "surrogate_best.pt" + + for iteration in range(1, n_iterations + 1): + print(f"\n{'='*60}") + print(f"ITERATION {iteration}/{n_iterations}") + print(f"{'='*60}") + + # Step 1: Train/Retrain surrogate from current database + print(f"\n[{iteration}.1] Training surrogate from current FEA data...") + surrogate = phase_train(results_dir, study_name, logger, epochs=epochs) + + # Step 2: Run NN optimization + print(f"\n[{iteration}.2] Running NN optimization ({nn_trials_per_iter} trials)...") + phase_nn_optimize(results_dir, study_name, surrogate, + n_trials=nn_trials_per_iter, logger=logger) + + # Step 3: Validate top candidates with FEA + print(f"\n[{iteration}.3] Validating top {validate_per_iter} candidates with FEA...") + validated = phase_validate( + results_dir, model_dir, config, surrogate, + n_validate=validate_per_iter, logger=logger, + add_to_main_study=True, study_name=study_name + ) + + # Check convergence (if errors are low enough) + if validated: + avg_errors = np.array([r['errors_percent'] for r in validated]).mean(axis=0) + max_error = max(avg_errors) + + print(f"\n Iteration {iteration} summary:") + print(f" Average errors: mass={avg_errors[0]:.1f}%, stress={avg_errors[1]:.1f}%, stiffness={avg_errors[2]:.1f}%") + print(f" Max error: {max_error:.1f}%") + + if max_error < 5.0: + print(f"\n ✓ Convergence reached! Max error < 5%") + break + + # Final summary + print(f"\n{'#'*60}") + print("# HYBRID LOOP COMPLETE") + print(f"{'#'*60}") + + # Load final study stats + main_db_path = results_dir / "study.db" + main_storage = f"sqlite:///{main_db_path}" + main_study = optuna.load_study(study_name=study_name, storage=main_storage) + + print(f"Total FEA trials in study: {len(main_study.trials)}") + print(f"Pareto front size: {len(main_study.best_trials)}") + + return surrogate + + +def phase_turbo_loop(results_dir: Path, model_dir: Path, config: dict, + study_name: str, total_nn_trials: int = 10000, + nn_batch_size: int = 100, retrain_every: int = 10, + epochs: int = 150, logger=None): + """ + TURBO MODE: Aggressive adaptive optimization. + + 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 + + This is more efficient than batch validation because: + - We always explore the most promising direction + - FEA validates where we actually care + - Surrogate improves continuously + + Example: 10K NN trials with batch=100 → 100 FEA validations + """ + + print(f"\n{'#'*60}") + print("# TURBO MODE: Aggressive Adaptive Optimization") + print(f"{'#'*60}") + print(f"Total NN budget: {total_nn_trials:,} trials") + print(f"NN batch size: {nn_batch_size}") + print(f"FEA validations: ~{total_nn_trials // nn_batch_size}") + print(f"Retrain every: {retrain_every} FEA runs") + + model_path = results_dir / "surrogate_best.pt" + nx_solver = NXSolver(nastran_version="2506") + sim_file = model_dir / config['simulation']['sim_file'] + + # Initial training + print(f"\n[INIT] Training initial surrogate...") + surrogate = phase_train(results_dir, study_name, logger, epochs=epochs) + + # Tracking + fea_count = 0 + nn_count = 0 + best_solutions = [] + iteration = 0 + + import time + start_time = time.time() + + while nn_count < total_nn_trials: + iteration += 1 + batch_trials = min(nn_batch_size, total_nn_trials - nn_count) + + print(f"\n{'─'*50}") + print(f"Iteration {iteration}: NN trials {nn_count+1}-{nn_count+batch_trials}") + + # Run NN batch (in-memory, no database) + best_candidate = None + best_score = float('inf') + + for _ in range(batch_trials): + # Random sample in design space + support_angle = np.random.uniform(20.0, 70.0) + tip_thickness = np.random.uniform(30.0, 60.0) + + params = {'support_angle': support_angle, 'tip_thickness': tip_thickness} + pred = surrogate.predict(params) + + # Score: weighted combination (lower is better) + # Adjust weights based on what matters most + score = pred['mass'] + 0.01 * pred['stress'] + 0.1 * abs(pred['stiffness']) + + if score < best_score: + best_score = score + best_candidate = { + 'params': params, + 'nn_pred': pred + } + + nn_count += batch_trials + + # Validate best candidate with FEA + params = best_candidate['params'] + nn_pred = best_candidate['nn_pred'] + + print(f" Best NN: angle={params['support_angle']:.1f}, thick={params['tip_thickness']:.1f}") + print(f" NN → mass={nn_pred['mass']:.4f}, stress={nn_pred['stress']:.1f}, stiff={nn_pred['stiffness']:.1f}") + + # Run FEA + result = nx_solver.run_simulation( + sim_file=sim_file, + working_dir=model_dir, + expression_updates=params, + solution_name=config['simulation'].get('solution_name'), + cleanup=True + ) + + if not result['success']: + print(f" FEA FAILED - skipping") + continue + + # Extract FEA results + op2_file = result['op2_file'] + dat_file = model_dir / config['simulation']['dat_file'] + + fea_mass = extract_mass_from_bdf(str(dat_file)) + stress_result = extract_solid_stress(op2_file, subcase=1, element_type='chexa') + fea_stress = stress_result.get('max_von_mises', float('inf')) / 1000.0 + disp_result = extract_displacement(op2_file, subcase=1) + max_disp = disp_result['max_displacement'] + fea_stiffness = -1000.0 / max(abs(max_disp), 1e-6) + + print(f" FEA → mass={fea_mass:.4f}, stress={fea_stress:.1f}, stiff={fea_stiffness:.1f}") + + # Compute prediction error + mass_err = abs(fea_mass - nn_pred['mass']) / fea_mass * 100 + stress_err = abs(fea_stress - nn_pred['stress']) / fea_stress * 100 + print(f" Error: mass={mass_err:.1f}%, stress={stress_err:.1f}%") + + fea_count += 1 + + # Add to main study database + main_db_path = results_dir / "study.db" + main_storage = f"sqlite:///{main_db_path}" + + try: + main_study = optuna.load_study( + study_name=study_name, + storage=main_storage, + sampler=NSGAIISampler(population_size=20, seed=42) + ) + + trial = main_study.ask() + trial.suggest_float('support_angle', params['support_angle'], params['support_angle']) + trial.suggest_float('tip_thickness', params['tip_thickness'], params['tip_thickness']) + main_study.tell(trial, [fea_mass, fea_stress, fea_stiffness]) + + trial.set_user_attr('source', 'turbo_mode') + trial.set_user_attr('iteration', iteration) + + except Exception as e: + print(f" Warning: couldn't add to study: {e}") + + # Track best solutions + best_solutions.append({ + 'iteration': iteration, + 'params': params, + 'fea': [fea_mass, fea_stress, fea_stiffness], + 'nn_error': [mass_err, stress_err] + }) + + # Retrain periodically + if fea_count % retrain_every == 0: + print(f"\n [RETRAIN] Retraining surrogate with {len(main_study.trials)} samples...") + surrogate = phase_train(results_dir, study_name, logger, epochs=epochs) + + # Progress + elapsed = time.time() - start_time + rate = nn_count / elapsed if elapsed > 0 else 0 + remaining = (total_nn_trials - nn_count) / rate if rate > 0 else 0 + print(f" Progress: {nn_count:,}/{total_nn_trials:,} NN | {fea_count} FEA | {elapsed/60:.1f}min elapsed | ~{remaining/60:.1f}min remaining") + + # Final summary + print(f"\n{'#'*60}") + print("# TURBO MODE COMPLETE") + print(f"{'#'*60}") + print(f"NN trials: {nn_count:,}") + print(f"FEA validations: {fea_count}") + print(f"Time: {(time.time() - start_time)/60:.1f} minutes") + + # Load final study + main_study = optuna.load_study(study_name=study_name, storage=main_storage) + print(f"Total trials in study: {len(main_study.trials)}") + print(f"Pareto front: {len(main_study.best_trials)} solutions") + + # Save turbo results + turbo_report = { + 'mode': 'turbo', + 'total_nn_trials': nn_count, + 'fea_validations': fea_count, + 'time_minutes': (time.time() - start_time) / 60, + 'best_solutions': best_solutions[-20:] # Last 20 + } + + report_path = results_dir / "turbo_report.json" + with open(report_path, 'w') as f: + json.dump(turbo_report, f, indent=2) + + print(f"\nReport saved to {report_path}") + + return surrogate + + +def load_config(config_file: Path) -> dict: + """Load configuration from JSON file.""" + with open(config_file, 'r') as f: + return json.load(f) + + +def main(): + """Main workflow.""" + parser = argparse.ArgumentParser(description='bracket_pareto_3obj - Hybrid NN Optimization') + + # Phase selection + parser.add_argument('--export', action='store_true', help='Phase 1: Export training data') + parser.add_argument('--train', action='store_true', help='Phase 2: Train surrogate') + parser.add_argument('--nn-optimize', action='store_true', help='Phase 3: NN optimization') + parser.add_argument('--validate', action='store_true', help='Phase 4: FEA validation') + parser.add_argument('--all', action='store_true', help='Run all phases once') + parser.add_argument('--hybrid-loop', action='store_true', + help='Run adaptive hybrid loop: Train→NN→Validate→Retrain (repeats)') + parser.add_argument('--turbo', action='store_true', + help='TURBO: Run 100 NN, validate best, retrain, repeat for 10K total') + + # Parameters + parser.add_argument('--epochs', type=int, default=300, help='Training epochs') + parser.add_argument('--nn-trials', type=int, default=1000, help='NN optimization trials (or total for turbo)') + parser.add_argument('--validate-count', type=int, default=10, help='Number of candidates to validate') + parser.add_argument('--iterations', type=int, default=3, help='Hybrid loop iterations') + parser.add_argument('--batch-size', type=int, default=100, help='NN batch size for turbo mode') + parser.add_argument('--retrain-every', type=int, default=10, help='Retrain surrogate every N FEA runs (turbo)') + + args = parser.parse_args() + + if not any([args.export, args.train, args.nn_optimize, args.validate, args.all, args.hybrid_loop, args.turbo]): + print("No phase specified. Use --export, --train, --nn-optimize, --validate, --all, --hybrid-loop, or --turbo") + 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" + model_path = results_dir / "surrogate_best.pt" + + # Initialize + logger = get_logger(study_name, study_dir=results_dir) + config = load_config(config_path) + + print(f"\n{'#'*60}") + print(f"# Bracket Pareto 3-Objective - Hybrid NN Optimization") + print(f"{'#'*60}") + print(f"Study: {study_name}") + print(f"Results: {results_dir}") + + # Execute phases + surrogate = None + + if args.all or args.export: + phase_export(results_dir, study_name, logger) + + if args.all or args.train: + surrogate = phase_train(results_dir, study_name, logger, epochs=args.epochs) + + if args.all or args.nn_optimize: + if surrogate is None: + if model_path.exists(): + surrogate = BracketSurrogate(model_path=model_path) + else: + print("ERROR: No trained surrogate. Run --train first.") + return 1 + + phase_nn_optimize(results_dir, study_name, surrogate, n_trials=args.nn_trials, logger=logger) + + if args.all or args.validate: + if surrogate is None: + if model_path.exists(): + surrogate = BracketSurrogate(model_path=model_path) + else: + print("ERROR: No trained surrogate. Run --train first.") + return 1 + + phase_validate(results_dir, model_dir, config, surrogate, + n_validate=args.validate_count, logger=logger, + study_name=study_name) + + # Hybrid loop mode - adaptive refinement + if args.hybrid_loop: + phase_hybrid_loop( + results_dir=results_dir, + model_dir=model_dir, + config=config, + study_name=study_name, + n_iterations=args.iterations, + nn_trials_per_iter=args.nn_trials, + validate_per_iter=args.validate_count, + epochs=args.epochs, + logger=logger + ) + + # Turbo mode - aggressive single-best validation + if args.turbo: + phase_turbo_loop( + results_dir=results_dir, + model_dir=model_dir, + config=config, + study_name=study_name, + total_nn_trials=args.nn_trials, + nn_batch_size=args.batch_size, + retrain_every=args.retrain_every, + epochs=args.epochs, + logger=logger + ) + + print(f"\n{'#'*60}") + print("# Workflow Complete!") + print(f"{'#'*60}\n") + + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/studies/bracket_pareto_3obj/run_optimization.py b/studies/bracket_pareto_3obj/run_optimization.py new file mode 100644 index 00000000..8f58f7da --- /dev/null +++ b/studies/bracket_pareto_3obj/run_optimization.py @@ -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())