auto: daily sync

This commit is contained in:
2026-02-15 08:00:21 +00:00
parent 6218355dbf
commit d6a1d6eee1
376 changed files with 12864 additions and 5377 deletions

View File

@@ -0,0 +1,514 @@
# Optimization Strategy — Hydrotech Beam DoE & Landscape Mapping
**Study:** `01_doe_landscape`
**Project:** Hydrotech Beam Structural Optimization
**Author:** ⚡ Optimizer Agent
**Date:** 2025-02-09 (updated 2026-02-10 — introspection corrections)
**Status:** APPROVED WITH CONDITIONS — Auditor review 2026-02-10, blockers resolved inline
**References:** [BREAKDOWN.md](../../BREAKDOWN.md), [DECISIONS.md](../../DECISIONS.md), [CONTEXT.md](../../CONTEXT.md)
---
## 1. Problem Formulation
### 1.1 Objective
$$\min_{x} \quad f(x) = \text{mass}(x) \quad [\text{kg}]$$
Single-objective minimization of total beam mass. This aligns with DEC-HB-001 (approved by Tech Lead, pending CEO confirmation).
### 1.2 Constraints
| ID | Constraint | Operator | Limit | Units | Source |
|----|-----------|----------|-------|-------|--------|
| g₁ | Tip displacement | ≤ | 10.0 | mm | NX Nastran SOL 101 — displacement sensor at beam tip |
| g₂ | Max von Mises stress | ≤ | 130.0 | MPa | NX Nastran SOL 101 — max elemental nodal VM stress |
Both are **hard constraints** — no trade-off or relaxation without CEO approval.
### 1.3 Design Variables
| ID | NX Expression | Type | Lower | Upper | Baseline | Units | Notes |
|----|---------------|------|-------|-------|----------|-------|-------|
| DV1 | `beam_half_core_thickness` | Continuous | 10 | 40 | **25.162** | mm | Core half-thickness; stiffness scales ~quadratically via sandwich effect |
| DV2 | `beam_face_thickness` | Continuous | 10 | 40 | **21.504** | mm | Face sheet thickness; primary bending stiffness contributor |
| DV3 | `holes_diameter` | Continuous | 150 | 450 | 300 | mm | Lightening hole diameter; mass ∝ d² reduction |
| DV4 | `hole_count` (→ `Pattern_p7`) | **Integer** | 5 | 15 | 10 | — | Number of lightening holes; 11 discrete levels |
**Total design space:** 3 continuous × 1 integer (11 levels) = effectively 3D continuous × 11 slices.
### 1.4 Integer Handling
Per DEC-HB-003, `hole_count` is treated as a **true integer** throughout:
- **Phase 1 (LHS):** Generate continuous LHS, round DV4 to nearest integer. Use stratified integer sampling to ensure coverage across all 11 levels.
- **Phase 2 (TPE):** Optuna `IntDistribution(5, 15)` — native integer support, no rounding hacks.
- **NX rebuild:** The model requires integer hole count. Non-integer values will cause geometry failures.
### 1.5 Baseline Assessment
| Metric | Baseline Value | Constraint | Status |
|--------|---------------|------------|--------|
| Mass | **1,133.01 kg** | (minimize) | Overbuilt — room to reduce |
| Tip displacement | ~22 mm (unverified — awaiting baseline re-run) | ≤ 10 mm | ❌ **Likely FAILS** |
| VM stress | (unknown — awaiting baseline re-run) | ≤ 130 MPa | ⚠️ Unconfirmed |
> ⚠️ **Critical:** The baseline design likely **violates** the displacement constraint (~22 mm vs 10 mm limit). Baseline re-run pending — CEO running SOL 101 in parallel. The optimizer must first find the feasible region before it can meaningfully minimize mass. This shapes the entire strategy.
>
> **Introspection note (2026-02-10):** Mass expression is `p173` (body_property147.mass, kg). DV baselines are NOT round numbers (face=21.504mm, core=25.162mm). NX expression `beam_lenght` has a typo (no 'h'). `hole_count` links to `Pattern_p7` in the NX pattern feature.
---
## 2. Algorithm Selection
### 2.1 Tech Lead's Recommendation
DEC-HB-002 proposes a two-phase strategy:
- **Phase 1:** Latin Hypercube Sampling (LHS) — 4050 trials
- **Phase 2:** TPE via Optuna — 60100 trials
### 2.2 My Assessment: **CONFIRMED with refinements**
The two-phase approach is the right call. Here's why, and what I'd adjust:
#### Why LHS → TPE is correct for this problem
| Factor | Implication | Algorithm Fit |
|--------|------------|---------------|
| 4 design variables (low-dim) | All methods work; sample efficiency less critical | Any |
| 1 integer variable | Need native mixed-type support | TPE ✓, CMA-ES ≈ (rounding) |
| Infeasible baseline | Must map feasibility BEFORE optimizing | LHS first ✓ |
| Expected significant interactions (DV1×DV2, DV3×DV4) | Need space-filling to detect interactions | LHS ✓ |
| Potentially narrow feasible region | Risk of missing it with random search | LHS gives systematic coverage ✓ |
| NX-in-the-loop (medium cost) | ~100-200 trials is budget-appropriate | TPE efficient enough ✓ |
#### What I'd modify
1. **Phase 1 budget: 50 trials (not 40).** With 4 variables, we want at least 10× the dimensionality for a reliable DoE. 50 trials also divides cleanly for stratified integer sampling (≈4-5 trials per hole_count level).
2. **Enqueue baseline as Trial 0.** LAC critical lesson: CMA-ES doesn't evaluate x0 first. While we're using LHS (not CMA-ES), the same principle applies — **always evaluate the baseline explicitly** so we have a verified anchor point. This also validates the extractor pipeline before burning 50 trials.
3. **Phase 2 budget: 80 trials (flexible 60-100).** Start with 60, apply convergence criteria (Section 6), extend to 100 if still improving.
4. **Seed Phase 2 from Phase 1 data.** Use Optuna's `enqueue_trial()` to warm-start TPE with the best feasible point(s) from the DoE. This avoids the TPE startup penalty (first `n_startup_trials` are random).
#### Algorithms NOT selected (and why)
| Algorithm | Why Not |
|-----------|---------|
| **CMA-ES** | Good option, but integer rounding is a hack; doesn't evaluate x0 first (LAC lesson); TPE is equally good at 4D |
| **NSGA-II** | Overkill for single-objective; population size wastes budget |
| **Surrogate + L-BFGS** | **LAC CRITICAL: Gradient descent on surrogates finds fake optima.** V5 mirror study: L-BFGS was 22% WORSE than pure TPE (WS=325 vs WS=290). V6 confirmed simple TPE beats complex surrogate methods. Do not use. |
| **SOL 200 (Nastran native)** | No integer support for hole_count; gradient-based so may miss global optimum; more NX setup effort. Keep as backup (Tech Lead's suggestion). |
| **Nelder-Mead** | No integer support; poor exploration; would miss the feasible region |
### 2.3 Final Algorithm Configuration
```
Phase 1: LHS DoE
- Trials: 50 (+ 1 baseline = 51 total)
- Sampling: Maximin LHS, DV4 rounded to nearest integer
- Purpose: Landscape mapping, feasibility identification, sensitivity analysis
Phase 2: TPE Optimization
- Trials: 60-100 (adaptive, see convergence criteria)
- Sampler: Optuna TPEsampler
- n_startup_trials: 0 (warm-started from Phase 1 best)
- Constraint handling: Optuna constraint interface with Deb's rules
- Purpose: Converge to minimum-mass feasible design
Total budget: 111-151 evaluations
```
---
## 3. Constraint Handling
### 3.1 The Challenge
The baseline FAILS the displacement constraint by 120% (22 mm vs 10 mm). This means:
- A significant portion of the design space may be infeasible
- Random sampling may return few or zero feasible points
- The optimizer must navigate toward feasibility AND optimality simultaneously
### 3.2 Approach: Deb's Feasibility Rules (Constraint Domination)
For ranking solutions during optimization, use **Deb's feasibility rules** (Deb 2000):
1. **Feasible vs feasible** → compare by objective (lower mass wins)
2. **Feasible vs infeasible** → feasible always wins
3. **Infeasible vs infeasible** → lower total constraint violation wins
This is implemented via Optuna's constraint interface:
```python
def constraints(trial):
"""Return constraint violations (negative = feasible, positive = infeasible)."""
disp = trial.user_attrs["tip_displacement"]
stress = trial.user_attrs["max_von_mises"]
return [
disp - 10.0, # ≤ 0 means displacement ≤ 10 mm
stress - 130.0, # ≤ 0 means stress ≤ 130 MPa
]
```
### 3.3 Why NOT Penalty Functions
| Method | Pros | Cons | Verdict |
|--------|------|------|---------|
| **Deb's rules** (selected) | No tuning params; feasible always beats infeasible; explores infeasible region for learning | Requires custom Optuna integration | ✅ Best for this case |
| **Quadratic penalty** | Simple to implement | Penalty weight requires tuning; wrong weight → optimizer ignores constraint OR over-penalizes | ❌ Fragile |
| **Adaptive penalty** | Self-tuning | Complex implementation; may oscillate | ❌ Over-engineered for 4 DVs |
| **Death penalty** (reject infeasible) | Simplest | With infeasible baseline, may reject 80%+ of trials → wasted budget | ❌ Dangerous |
### 3.4 Phase 1 (DoE) Constraint Handling
During the DoE phase, **record all results without filtering.** Every trial runs, every result is stored. Infeasible points are valuable for:
- Mapping the feasibility boundary
- Training the TPE model in Phase 2
- Understanding which variables drive constraint violation
### 3.5 Constraint Margin Buffer
Consider a 5% inner margin during optimization to account for numerical noise:
- Displacement target for optimizer: ≤ 9.5 mm (vs hard limit 10.0 mm)
- Stress target for optimizer: ≤ 123.5 MPa (vs hard limit 130.0 MPa)
The hard limits remain 10 mm / 130 MPa for final validation. The buffer prevents the optimizer from converging to designs that are right on the boundary and may flip infeasible under mesh variation.
---
## 4. Search Space Analysis
### 4.1 Bound Reasonableness
| Variable | Range | Span | Concern |
|----------|-------|------|---------|
| DV1: half_core_thickness | 1040 mm | 4× range | Reasonable. Lower bound = thin core, upper = thick. Stiffness-mass trade-off |
| DV2: face_thickness | 1040 mm | 4× range | Reasonable. 10 mm face is already substantial for steel |
| DV3: holes_diameter | 150450 mm | 3× range | ⚠️ **Needs geometric check** — see §4.2 |
| DV4: hole_count | 515 | 3× range | ⚠️ **Needs geometric check** — see §4.2 |
### 4.2 Geometric Feasibility: Hole Overlap Analysis
**Critical concern:** At extreme DV3 × DV4 combinations, holes may overlap or leave insufficient ligament (material between holes).
#### Overlap condition (CORRECTED — Auditor review 2026-02-10)
The NX pattern places `n` holes across a span of `p6` mm using `n-1` intervals (holes at both endpoints of the span). Confirmed by introspection: `Pattern_p8 = 4000/9 = 444.44 mm`.
```
Spacing between hole centers = hole_span / (hole_count - 1)
Ligament between holes = spacing - d = hole_span/(hole_count - 1) - d
```
For **no overlap**, we need: `hole_span/(n-1) - d > 0`, i.e., `d < hole_span/(n-1)`
With `hole_span = 4,000 mm` (fixed, `p6`):
#### Worst case: n=15 holes, d=450 mm
```
Spacing = 4000 / (15-1) = 285.7 mm
Ligament = 285.7 - 450 = -164.3 mm → INFEASIBLE (overlap)
```
#### Minimum ligament width
For structural integrity and mesh quality, a minimum ligament of ~30 mm is advisable:
```
Minimum ligament constraint: hole_span / (hole_count - 1) - holes_diameter ≥ 30 mm
```
#### Pre-flight geometric filter
Before sending any trial to NX, compute:
1. `ligament = 4000 / (hole_count - 1) - holes_diameter` → must be ≥ 30 mm
2. `web_clear = 2 × beam_half_height - 2 × beam_face_thickness - holes_diameter` → must be > 0
If either fails, skip NX evaluation and record as infeasible with max constraint violation. This saves compute and avoids NX geometry crashes.
### 4.3 Hole-to-Web-Height Ratio (CORRECTED — Auditor review 2026-02-10)
The hole diameter must fit within the web clear height. From introspection:
- Total beam height = `2 × beam_half_height = 2 × 250 = 500 mm` (fixed)
- Web clear height = `total_height - 2 × face_thickness = 500 - 2 × beam_face_thickness`
```
At baseline (face=21.504mm): web_clear = 500 - 2×21.504 = 456.99 mm → holes of 450mm barely fit (7mm clearance)
At face=40mm: web_clear = 500 - 2×40 = 420 mm → holes of 450mm DO NOT FIT
At face=10mm: web_clear = 500 - 2×10 = 480 mm → holes of 450mm fit (30mm clearance)
```
This means `beam_face_thickness` and `holes_diameter` interact geometrically — thicker faces reduce the web clear height available for holes. This constraint is captured in the pre-flight filter (§4.2):
```
web_clear = 500 - 2 × beam_face_thickness - holes_diameter > 0
```
### 4.4 Expected Feasible Region
Based on the physics (Tech Lead's analysis §1.2 and §1.3):
| To reduce displacement (currently 22→10 mm) | Effect on mass |
|----------------------------------------------|---------------|
| ↑ DV1 (thicker core) | ↑ mass (but stiffness scales ~d², mass scales ~d) → **efficient** |
| ↑ DV2 (thicker face) | ↑ mass (direct) |
| ↓ DV3 (smaller holes) | ↑ mass (more web material) |
| ↓ DV4 (fewer holes) | ↑ mass (more web material) |
**Prediction:** The feasible region (displacement ≤ 10 mm) likely requires:
- DV1 in upper range (25-40 mm) — the sandwich effect is the most mass-efficient stiffness lever
- DV2 moderate (15-30 mm) — thicker faces help stiffness but cost mass directly
- DV3 and DV4 constrained by stress — large/many holes save mass but increase stress
The optimizer should find a "sweet spot" where core thickness provides stiffness, and holes are sized to save mass without violating stress limits.
### 4.5 Estimated Design Space Volume
- DV1: 30 mm span (continuous)
- DV2: 30 mm span (continuous)
- DV3: 300 mm span (continuous)
- DV4: 11 integer levels
Total configurations: effectively infinite (3 continuous), but the integer dimension creates 11 "slices" of the space. With 50 DoE trials, we get ~4-5 trials per slice — sufficient for trend identification.
---
## 5. Trial Budget & Compute Estimate
### 5.1 Budget Breakdown
| Phase | Trials | Purpose |
|-------|--------|---------|
| **Trial 0** | 1 | Baseline validation (enqueued) |
| **Phase 1: LHS DoE** | 50 | Landscape mapping, feasibility, sensitivity |
| **Phase 2: TPE** | 60100 | Directed optimization |
| **Validation** | 35 | Confirm optimum, check mesh sensitivity |
| **Total** | **114156** | |
### 5.2 Compute Time Estimate
| Parameter | Estimate | Notes |
|-----------|----------|-------|
| DOF count | 10K100K | Steel beam, SOL 101 |
| Single solve time | 30s3min | Depends on mesh density |
| Model rebuild time | 1030s | NX parametric update + remesh |
| Total per trial | 14 min | Rebuild + solve + extraction |
| Phase 1 (51 trials) | 13.5 hrs | |
| Phase 2 (60100 trials) | 17 hrs | |
| **Total compute** | **210 hrs** | Likely ~45 hrs |
### 5.3 Budget Justification
For 4 design variables, rule-of-thumb budgets:
- **Minimum viable:** 10 × n_vars = 40 trials (DoE only)
- **Standard:** 25 × n_vars = 100 trials (DoE + optimization)
- **Thorough:** 50 × n_vars = 200 trials (with validation)
Our budget of 114156 falls in the **standard-to-thorough** range. Appropriate for a first study where we're mapping an unknown landscape with an infeasible baseline.
---
## 6. Convergence Criteria
### 6.1 Phase 1 (DoE) — No Convergence Criteria
The DoE runs all 50 planned trials. It's not iterative — it's a one-shot space-filling design. Stop conditions:
- All 50 trials complete (or fail with documented errors)
- **Early abort:** If >80% of trials fail to solve (NX crashes), stop and investigate
### 6.2 Phase 2 (TPE) — Convergence Criteria
| Criterion | Threshold | Action |
|-----------|-----------|--------|
| **Improvement stall** | Best feasible objective unchanged for 20 consecutive trials | Consider stopping |
| **Relative improvement** | < 1% improvement over last 20 trials | Consider stopping |
| **Budget exhausted** | 100 trials completed in Phase 2 | Hard stop |
| **Perfect convergence** | Multiple trials within 0.5% of each other from different regions | Confident optimum found |
| **Minimum budget** | Always run at least 60 trials in Phase 2 | Ensures adequate exploration |
### 6.3 Decision Logic
```
After 60 Phase 2 trials:
IF best_feasible improved by >2% in last 20 trials → continue to 80
IF no feasible solution found → STOP, escalate (see §7.1)
ELSE → assess convergence, decide 80 or 100
After 80 Phase 2 trials:
IF still improving >1% per 20 trials → continue to 100
ELSE → STOP, declare converged
After 100 Phase 2 trials:
HARD STOP regardless
```
### 6.4 Phase 1 → Phase 2 Gate
Before starting Phase 2, review DoE results:
| Check | Action if FAIL |
|-------|---------------|
| At least 5 feasible points found | If 0 feasible: expand bounds or relax constraints (escalate to CEO) |
| NX solve success rate > 80% | If <80%: investigate failures, fix model, re-run failed trials |
| No systematic NX crashes at bounds | If crashes: tighten bounds away from failure region |
| Sensitivity trends visible | If flat: check extractors, may be reading wrong output |
---
## 7. Risk Mitigation
### 7.1 Risk: Feasible Region is Empty
**Likelihood: Medium** (baseline fails displacement by 120%)
**Detection:** After Phase 1, zero feasible points found.
**Mitigation ladder:**
1. **Check the data** — Are extractors reading correctly? Validate against manual NX check.
2. **Examine near-feasible** — Find the trial closest to feasibility. How far off? If displacement = 10.5 mm, we're close. If displacement = 18 mm, we have a problem.
3. **Targeted exploration** — Run additional trials at extreme stiffness (max DV1, max DV2, min DV3, min DV4). If even the stiffest/heaviest design fails, the constraint is physically impossible with this geometry.
4. **Constraint relaxation** — Propose to CEO: relax displacement to 12 or 15 mm. Document the mass-displacement Pareto front from DoE data to support the discussion.
5. **Geometric redesign** — If the problem is fundamentally infeasible, the beam geometry needs redesign (out of optimization scope).
### 7.2 Risk: NX Crashes at Parameter Extremes
**Likelihood: Medium** (LAC: rib_thickness had undocumented CAD constraint at 9mm, causing 34% failure rate in V13)
**Detection:** Solver returns no results for certain parameter combinations.
**Mitigation:**
1. **Pre-flight corner tests** — Before Phase 1, manually test the 16 corners of the design space (2⁴ combinations of min/max for each variable). This catches geometric rebuild failures early.
2. **Error-handling in run script** — Every trial must catch exceptions and log:
- NX rebuild failure (geometry Boolean crash)
- Meshing failure (degenerate elements)
- Solver failure (singularity, divergence)
- Extraction failure (missing result)
3. **Infeasible-by-default** — If a trial fails for any reason, record it as infeasible with maximum constraint violation (displacement=9999, stress=9999). This lets Deb's rules naturally steer away from crashing regions.
4. **NEVER kill NX processes directly** — LAC CRITICAL RULE. Use NXSessionManager.close_nx_if_allowed() only. If NX hangs, implement a timeout (e.g., 10 min per trial) and let NX time out gracefully.
### 7.3 Risk: Mesh-Dependent Stress Results
**Likelihood: Medium** (stress at hole edges is mesh-sensitive)
**Mitigation:**
1. **Mesh convergence pre-study** — Run baseline at 3 mesh densities. If stress varies >10%, refine mesh or use stress averaging region.
2. **Consistent mesh controls** — Ensure NX applies the same mesh size/refinement strategy regardless of parameter values. The parametric model should have mesh controls tied to hole geometry.
3. **Stress extraction method** — Use elemental nodal stress (conservative) per LAC success pattern. Note: pyNastran returns stress in kPa for NX kg-mm-s unit system — **divide by 1000 for MPa**.
### 7.4 Risk: Surrogate Temptation
**Mitigation: DON'T DO IT (yet).**
LAC lessons from the M1 Mirror project are unequivocal:
- V5 surrogate + L-BFGS was 22% **worse** than V6 pure TPE
- MLP surrogates have smooth gradients everywhere → L-BFGS descends to fake optima outside training distribution
- No uncertainty quantification = no way to detect out-of-distribution predictions
With only 4 variables and affordable FEA (~2 min/trial), direct FEA evaluation via TPE is both simpler and more reliable. Surrogate methods should only be considered if:
- FEA solve time exceeds 30 minutes per trial, AND
- We have 100+ validated training points, AND
- We use ensemble surrogates with uncertainty quantification (SYS_16 protocol)
### 7.5 Risk: Study Corruption
**Mitigation:** LAC CRITICAL — **Always copy working studies, never rewrite from scratch.**
- Phase 2 study will be created by **copying** the Phase 1 study directory and adding optimization logic
- Never modify `run_optimization.py` in-place for a new phase — copy to a new version
- Git-commit the study directory after each phase completion
---
## 8. AtomizerSpec Draft
See [`atomizer_spec_draft.json`](./atomizer_spec_draft.json) for the full JSON config.
### 8.1 Key Configuration Decisions
| Setting | Value | Rationale |
|---------|-------|-----------|
| `algorithm.phase1.type` | `LHS` | Space-filling DoE for landscape mapping |
| `algorithm.phase2.type` | `TPE` | Native mixed-integer, sample-efficient, LAC-proven |
| `hole_count.type` | `integer` | DEC-HB-003: true integer, no rounding |
| `constraint_handling` | `deb_feasibility_rules` | Best for infeasible baseline |
| `baseline_trial` | `enqueued` | LAC lesson: always validate baseline first |
| `penalty_config.method` | `deb_rules` | No penalty weight tuning needed |
### 8.2 Extractor Requirements
| ID | Type | Output | Source | Notes |
|----|------|--------|--------|-------|
| `ext_001` | `expression` | `mass` | NX expression `p173` | Direct read from NX |
| `ext_002` | `displacement` | `tip_displacement` | SOL 101 result sensor or .op2 parse | ⚠️ Need sensor setup or node ID |
| `ext_003` | `stress` | `max_von_mises` | SOL 101 elemental nodal | kPa → MPa conversion needed |
### 8.3 Open Items for Spec Finalization
Before this spec can be promoted from `_draft` to production:
1. **Beam web length** — Required to validate DV3 × DV4 geometric feasibility
2. **Displacement extraction method** — Sensor in .sim, or node ID for .op2 parsing?
3. **Stress extraction scope** — Whole model max, or specific element group?
4. **NX expression names confirmed** — Verify `p173` is mass, confirm displacement/stress expression names
5. **Solver runtime benchmark** — Time one SOL 101 run to refine compute estimates
6. **Corner test results** — Validate model rebuilds at all 16 bound corners
---
## 9. Execution Plan Summary
```
┌─────────────────────────────────────────────────────────────────┐
│ HYDROTECH BEAM OPTIMIZATION │
│ Study: 01_doe_landscape │
├─────────────────────────────────────────────────────────────────┤
│ │
│ PRE-FLIGHT (before any trials) │
│ ├── Validate baseline: run Trial 0, verify mass/disp/stress │
│ ├── Corner tests: 16 extreme combinations, check NX rebuilds │
│ ├── Mesh convergence: 3 density levels at baseline │
│ └── Confirm extractors: mass, displacement, stress pipelines │
│ │
│ PHASE 1: DoE LANDSCAPE (51 trials) │
│ ├── Trial 0: Baseline (enqueued) │
│ ├── Trials 1-50: LHS with integer rounding for hole_count │
│ ├── Analysis: sensitivity, interaction, feasibility mapping │
│ └── GATE: ≥5 feasible? NX success >80%? Proceed/escalate │
│ │
│ PHASE 2: TPE OPTIMIZATION (60-100 trials) │
│ ├── Warm-start from best Phase 1 feasible point(s) │
│ ├── Deb's feasibility rules for constraint handling │
│ ├── Convergence check every 20 trials │
│ └── Hard stop at 100 trials │
│ │
│ VALIDATION (3-5 trials) │
│ ├── Re-run best design to confirm repeatability │
│ ├── Perturb ±5% on each variable to check sensitivity │
│ └── Document final design with full NX results │
│ │
│ TOTAL: 114-156 NX evaluations | ~4-5 hours compute │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 10. LAC Lessons Incorporated
| LAC Lesson | Source | How Applied |
|------------|--------|-------------|
| CMA-ES doesn't evaluate x0 first | Mirror V7 failure | Baseline enqueued as Trial 0 for both phases |
| Surrogate + L-BFGS = fake optima | Mirror V5 failure | No surrogates in this study; direct FEA only |
| Never kill NX processes directly | Dec 2025 incident | Timeout-based error handling; NXSessionManager only |
| Copy working studies, never rewrite | Mirror V5 failure | Phase 2 created by copying Phase 1 |
| pyNastran stress in kPa | Support arm success | Extractor divides by 1000 for MPa |
| CAD constraints can limit bounds | Mirror V13 (rib_thickness) | Pre-flight corner tests before DoE |
| Always include README.md | Repeated failures (Dec 2025, Jan 2026) | README.md created with study |
| Simple beats complex (TPE > surrogate) | Mirror V6 vs V5 | TPE selected over surrogate-based methods |
---
*⚡ Optimizer — The algorithm is the strategy.*

View File

@@ -1,235 +1,235 @@
# Study: 01_doe_landscape — Hydrotech Beam
> See [../../README.md](../../README.md) for project overview.
## Purpose
Map the design space of the Hydrotech sandwich I-beam to identify feasible regions, characterize variable sensitivities, and prepare data for Phase 2 (TPE optimization). This is Phase 1 of the two-phase strategy (DEC-HB-002).
## Quick Facts
| Item | Value |
|------|-------|
| **Objective** | Minimize mass (kg) |
| **Constraints** | Tip displacement ≤ 10 mm, Von Mises stress ≤ 130 MPa |
| **Design variables** | 4 (3 continuous + 1 integer) |
| **Algorithm** | Phase 1: LHS DoE (50 trials + 1 baseline) |
| **Total budget** | 51 evaluations |
| **Constraint handling** | Deb's feasibility rules (Optuna constraint interface) |
| **Status** | ✅ Pipeline operational — first results obtained 2026-02-11 |
| **Solver** | DesigncenterNX 2512 (NX Nastran SOL 101) |
| **Solve time** | ~12 s/trial (~10 min for full DOE) |
| **First results** | Displacement=17.93mm, Stress=111.9MPa |
## Design Variables (confirmed via NX binary introspection)
| ID | NX Expression | Range | Type | Baseline | Unit |
|----|--------------|-------|------|----------|------|
| DV1 | `beam_half_core_thickness` | 1040 | Continuous | 25.162 | mm |
| DV2 | `beam_face_thickness` | 1040 | Continuous | 21.504 | mm |
| DV3 | `holes_diameter` | 150450 | Continuous | 300 | mm |
| DV4 | `hole_count` | 515 | Integer | 10 | — |
## Usage
### Prerequisites
```bash
pip install -r requirements.txt
```
Requires: Python 3.10+, optuna, scipy, numpy, pandas.
### Development Run (stub solver)
```bash
# From the study directory:
cd projects/hydrotech-beam/studies/01_doe_landscape/
# Run with synthetic results (for pipeline testing):
python run_doe.py --backend stub
# With verbose logging:
python run_doe.py --backend stub -v
# Custom study name:
python run_doe.py --backend stub --study-name my_test_run
```
### Production Run (NXOpen on Windows)
```bash
# On dalidou (Windows node with DesigncenterNX 2512):
cd C:\Users\antoi\Atomizer\projects\hydrotech-beam\studies\01_doe_landscape
conda activate atomizer
python run_doe.py --backend nxopen --model-dir "../../models"
```
> ⚠️ `--model-dir` can be relative — the code resolves it with `Path.resolve()`. NX requires fully resolved paths (no `..` components).
### Resume Interrupted Study
```bash
python run_doe.py --backend stub --resume --study-name hydrotech_beam_doe_phase1
```
### CLI Options
| Flag | Default | Description |
|------|---------|-------------|
| `--backend` | `stub` | `stub` (testing) or `nxopen` (real NX) |
| `--model-dir` | — | Path to NX model files (required for `nxopen`) |
| `--study-name` | `hydrotech_beam_doe_phase1` | Optuna study name |
| `--n-samples` | 50 | Number of LHS sample points |
| `--seed` | 42 | Random seed for reproducibility |
| `--results-dir` | `results/` | Output directory |
| `--resume` | false | Resume existing study |
| `--clean` | false | Delete Optuna DB and start fresh (history DB preserved) |
| `-v` | false | Verbose (DEBUG) logging |
## Files
| File | Description |
|------|-------------|
| `run_doe.py` | Main entry point — orchestrates the DoE study |
| `sampling.py` | LHS generation with stratified integer sampling |
| `geometric_checks.py` | Pre-flight geometric feasibility filter |
| `nx_interface.py` | AtomizerNXSolver wrapping optimization_engine |
| `iteration_manager.py` | Smart retention — last 10 + best 3 with full data |
| `requirements.txt` | Python dependencies |
| `OPTIMIZATION_STRATEGY.md` | Full strategy document |
| `results/` | Output directory (CSV, JSON, Optuna DB) |
| `iterations/` | Per-trial archived outputs |
## Output Files
After a study run, `results/` will contain:
| File | Description |
|------|-------------|
| `doe_results.csv` | All trials — DVs, objectives, constraints, status |
| `doe_summary.json` | Study metadata, statistics, best feasible design |
| `optuna_study.db` | SQLite database (Optuna persistence, resume support) |
| `history.db` | **Persistent** SQLite — all DVs, results, feasibility (survives `--clean`) |
| `history.csv` | CSV mirror of history DB |
### Iteration Folders
Each trial produces `iterations/iterNNN/` containing:
| File | Description |
|------|-------------|
| `params.json` | Design variable values for this trial |
| `params.exp` | NX expression file used for this trial |
| `results.json` | Extracted results (mass, displacement, stress, feasibility) |
| `*.op2` | Nastran binary results (for post-processing) |
| `*.f06` | Nastran text output (for debugging) |
**Smart retention policy** (runs every 5 iterations):
- Keep last 10 iterations with full data
- Keep best 3 feasible iterations with full data
- Strip model files from all others, keep solver outputs
## Architecture
```
run_doe.py
├── sampling.py Generate 50 LHS + 1 baseline
│ └── scipy.stats.qmc.LatinHypercube
├── geometric_checks.py Pre-flight feasibility filter
│ ├── Hole overlap: span/(n-1) - d ≥ 30mm
│ └── Web clearance: 500 - 2·face - d > 0
├── nx_interface.py AtomizerNXSolver (wraps optimization_engine)
│ ├── NXStubSolver → synthetic results (development)
│ └── AtomizerNXSolver → real DesigncenterNX 2512 SOL 101 (production)
├── iteration_manager.py Smart retention (last 10 + best 3)
├── Optuna study
│ ├── SQLite storage (resume support)
│ ├── Enqueued trials (deterministic LHS)
│ └── Deb's feasibility rules (constraint interface)
└── history.db Persistent history (append-only, survives --clean)
```
## Pipeline per Trial
```
Trial N
├── 1. Suggest DVs (from enqueued LHS point)
├── 2. Geometric pre-check
│ ├── FAIL → record as infeasible, skip NX
│ └── PASS ↓
├── 3. Restore master model from backup
├── 4. NX evaluation (IN-PLACE in models/ directory)
│ ├── Write .exp file with trial DVs
│ ├── Open .sim (loads model chain)
│ ├── Import expressions, rebuild geometry
│ ├── Update FEM (remesh)
│ ├── Solve SOL 101 (~12s)
│ └── Extract: mass (p173 → _temp_mass.txt), displacement, stress (via OP2)
├── 5. Archive outputs to iterations/iterNNN/
│ └── params.json, params.exp, results.json, OP2, F06
├── 6. Record results + constraint violations
├── 7. Log to Optuna DB + history.db + CSV
└── 8. Smart retention check (every 5 iterations)
```
## Phase 1 → Phase 2 Gate Criteria
Before proceeding to Phase 2 (TPE optimization), these checks must pass:
| Check | Threshold | Action if FAIL |
|-------|-----------|----------------|
| Feasible points found | ≥ 5 | Expand bounds or relax constraints (escalate to CEO) |
| NX solve success rate | ≥ 80% | Investigate failures, fix model, re-run |
| No systematic crashes at bounds | Visual check | Tighten bounds away from failure region |
## Key Design Decisions
- **Baseline as Trial 0** — LAC lesson: always validate baseline first
- **Pre-flight geometric filter** — catches infeasible geometry before NX (saves compute, avoids crashes)
- **Stratified integer sampling** — ensures all 11 hole_count levels (5-15) are covered
- **Deb's feasibility rules** — no penalty weight tuning; feasible always beats infeasible
- **SQLite persistence** — study can be interrupted and resumed
- **No surrogates** — LAC lesson: direct FEA via TPE beats surrogate + L-BFGS
## NX Integration Notes
**Solver:** DesigncenterNX 2512 (Siemens rebrand of NX). Install: `C:\Program Files\Siemens\DesigncenterNX2512`.
The `nx_interface.py` module provides:
- **`NXStubSolver`** — synthetic results from simplified beam mechanics (for pipeline testing)
- **`AtomizerNXSolver`** — wraps `optimization_engine` NXSolver for real DesigncenterNX 2512 solves
Expression names are exact from binary introspection. Critical: `beam_lenght` has a typo in NX (no 'h') — use exact spelling.
### Outputs extracted from NX:
| Output | Source | Unit | Notes |
|--------|--------|------|-------|
| Mass | Expression `p173``_temp_mass.txt` | kg | Journal extracts after solve |
| Tip displacement | OP2 parse via pyNastran | mm | Max Tz at free end |
| Max VM stress | OP2 parse via pyNastran | MPa | ⚠️ pyNastran returns kPa — divide by 1000 |
### Critical Lessons Learned (2026-02-11)
1. **Path resolution:** Use `Path.resolve()` NOT `Path.absolute()` on Windows. `.absolute()` doesn't resolve `..` components — NX can't follow them.
2. **File references:** NX `.sim` files have absolute internal refs to `.fem`/`.prt`. Never copy model files to iteration folders — solve in-place with backup/restore.
3. **NX version:** Must match exactly. Model files from 2512 won't load in 2412 ("Part file is from a newer version").
4. **pyNastran:** Warns about unsupported NX version 2512 but works fine. Safe to ignore.
## References
- [OPTIMIZATION_STRATEGY.md](./OPTIMIZATION_STRATEGY.md) — Full strategy document
- [../../BREAKDOWN.md](../../BREAKDOWN.md) — Tech Lead's technical analysis
- [../../DECISIONS.md](../../DECISIONS.md) — Decision log
- [../../CONTEXT.md](../../CONTEXT.md) — Project context & expression map
---
*Code: 🏗️ Study Builder (Technical Lead) | Strategy: ⚡ Optimizer Agent | Updated: 2026-02-11*
# Study: 01_doe_landscape — Hydrotech Beam
> See [../../README.md](../../README.md) for project overview.
## Purpose
Map the design space of the Hydrotech sandwich I-beam to identify feasible regions, characterize variable sensitivities, and prepare data for Phase 2 (TPE optimization). This is Phase 1 of the two-phase strategy (DEC-HB-002).
## Quick Facts
| Item | Value |
|------|-------|
| **Objective** | Minimize mass (kg) |
| **Constraints** | Tip displacement ≤ 10 mm, Von Mises stress ≤ 130 MPa |
| **Design variables** | 4 (3 continuous + 1 integer) |
| **Algorithm** | Phase 1: LHS DoE (50 trials + 1 baseline) |
| **Total budget** | 51 evaluations |
| **Constraint handling** | Deb's feasibility rules (Optuna constraint interface) |
| **Status** | ✅ Pipeline operational — first results obtained 2026-02-11 |
| **Solver** | DesigncenterNX 2512 (NX Nastran SOL 101) |
| **Solve time** | ~12 s/trial (~10 min for full DOE) |
| **First results** | Displacement=17.93mm, Stress=111.9MPa |
## Design Variables (confirmed via NX binary introspection)
| ID | NX Expression | Range | Type | Baseline | Unit |
|----|--------------|-------|------|----------|------|
| DV1 | `beam_half_core_thickness` | 1040 | Continuous | 25.162 | mm |
| DV2 | `beam_face_thickness` | 1040 | Continuous | 21.504 | mm |
| DV3 | `holes_diameter` | 150450 | Continuous | 300 | mm |
| DV4 | `hole_count` | 515 | Integer | 10 | — |
## Usage
### Prerequisites
```bash
pip install -r requirements.txt
```
Requires: Python 3.10+, optuna, scipy, numpy, pandas.
### Development Run (stub solver)
```bash
# From the study directory:
cd projects/hydrotech-beam/studies/01_doe_landscape/
# Run with synthetic results (for pipeline testing):
python run_doe.py --backend stub
# With verbose logging:
python run_doe.py --backend stub -v
# Custom study name:
python run_doe.py --backend stub --study-name my_test_run
```
### Production Run (NXOpen on Windows)
```bash
# On dalidou (Windows node with DesigncenterNX 2512):
cd C:\Users\antoi\Atomizer\projects\hydrotech-beam\studies\01_doe_landscape
conda activate atomizer
python run_doe.py --backend nxopen --model-dir "../../models"
```
> ⚠️ `--model-dir` can be relative — the code resolves it with `Path.resolve()`. NX requires fully resolved paths (no `..` components).
### Resume Interrupted Study
```bash
python run_doe.py --backend stub --resume --study-name hydrotech_beam_doe_phase1
```
### CLI Options
| Flag | Default | Description |
|------|---------|-------------|
| `--backend` | `stub` | `stub` (testing) or `nxopen` (real NX) |
| `--model-dir` | — | Path to NX model files (required for `nxopen`) |
| `--study-name` | `hydrotech_beam_doe_phase1` | Optuna study name |
| `--n-samples` | 50 | Number of LHS sample points |
| `--seed` | 42 | Random seed for reproducibility |
| `--results-dir` | `results/` | Output directory |
| `--resume` | false | Resume existing study |
| `--clean` | false | Delete Optuna DB and start fresh (history DB preserved) |
| `-v` | false | Verbose (DEBUG) logging |
## Files
| File | Description |
|------|-------------|
| `run_doe.py` | Main entry point — orchestrates the DoE study |
| `sampling.py` | LHS generation with stratified integer sampling |
| `geometric_checks.py` | Pre-flight geometric feasibility filter |
| `nx_interface.py` | AtomizerNXSolver wrapping optimization_engine |
| `iteration_manager.py` | Smart retention — last 10 + best 3 with full data |
| `requirements.txt` | Python dependencies |
| `OPTIMIZATION_STRATEGY.md` | Full strategy document |
| `results/` | Output directory (CSV, JSON, Optuna DB) |
| `iterations/` | Per-trial archived outputs |
## Output Files
After a study run, `results/` will contain:
| File | Description |
|------|-------------|
| `doe_results.csv` | All trials — DVs, objectives, constraints, status |
| `doe_summary.json` | Study metadata, statistics, best feasible design |
| `optuna_study.db` | SQLite database (Optuna persistence, resume support) |
| `history.db` | **Persistent** SQLite — all DVs, results, feasibility (survives `--clean`) |
| `history.csv` | CSV mirror of history DB |
### Iteration Folders
Each trial produces `iterations/iterNNN/` containing:
| File | Description |
|------|-------------|
| `params.json` | Design variable values for this trial |
| `params.exp` | NX expression file used for this trial |
| `results.json` | Extracted results (mass, displacement, stress, feasibility) |
| `*.op2` | Nastran binary results (for post-processing) |
| `*.f06` | Nastran text output (for debugging) |
**Smart retention policy** (runs every 5 iterations):
- Keep last 10 iterations with full data
- Keep best 3 feasible iterations with full data
- Strip model files from all others, keep solver outputs
## Architecture
```
run_doe.py
├── sampling.py Generate 50 LHS + 1 baseline
│ └── scipy.stats.qmc.LatinHypercube
├── geometric_checks.py Pre-flight feasibility filter
│ ├── Hole overlap: span/(n-1) - d ≥ 30mm
│ └── Web clearance: 500 - 2·face - d > 0
├── nx_interface.py AtomizerNXSolver (wraps optimization_engine)
│ ├── NXStubSolver → synthetic results (development)
│ └── AtomizerNXSolver → real DesigncenterNX 2512 SOL 101 (production)
├── iteration_manager.py Smart retention (last 10 + best 3)
├── Optuna study
│ ├── SQLite storage (resume support)
│ ├── Enqueued trials (deterministic LHS)
│ └── Deb's feasibility rules (constraint interface)
└── history.db Persistent history (append-only, survives --clean)
```
## Pipeline per Trial
```
Trial N
├── 1. Suggest DVs (from enqueued LHS point)
├── 2. Geometric pre-check
│ ├── FAIL → record as infeasible, skip NX
│ └── PASS ↓
├── 3. Restore master model from backup
├── 4. NX evaluation (IN-PLACE in models/ directory)
│ ├── Write .exp file with trial DVs
│ ├── Open .sim (loads model chain)
│ ├── Import expressions, rebuild geometry
│ ├── Update FEM (remesh)
│ ├── Solve SOL 101 (~12s)
│ └── Extract: mass (p173 → _temp_mass.txt), displacement, stress (via OP2)
├── 5. Archive outputs to iterations/iterNNN/
│ └── params.json, params.exp, results.json, OP2, F06
├── 6. Record results + constraint violations
├── 7. Log to Optuna DB + history.db + CSV
└── 8. Smart retention check (every 5 iterations)
```
## Phase 1 → Phase 2 Gate Criteria
Before proceeding to Phase 2 (TPE optimization), these checks must pass:
| Check | Threshold | Action if FAIL |
|-------|-----------|----------------|
| Feasible points found | ≥ 5 | Expand bounds or relax constraints (escalate to CEO) |
| NX solve success rate | ≥ 80% | Investigate failures, fix model, re-run |
| No systematic crashes at bounds | Visual check | Tighten bounds away from failure region |
## Key Design Decisions
- **Baseline as Trial 0** — LAC lesson: always validate baseline first
- **Pre-flight geometric filter** — catches infeasible geometry before NX (saves compute, avoids crashes)
- **Stratified integer sampling** — ensures all 11 hole_count levels (5-15) are covered
- **Deb's feasibility rules** — no penalty weight tuning; feasible always beats infeasible
- **SQLite persistence** — study can be interrupted and resumed
- **No surrogates** — LAC lesson: direct FEA via TPE beats surrogate + L-BFGS
## NX Integration Notes
**Solver:** DesigncenterNX 2512 (Siemens rebrand of NX). Install: `C:\Program Files\Siemens\DesigncenterNX2512`.
The `nx_interface.py` module provides:
- **`NXStubSolver`** — synthetic results from simplified beam mechanics (for pipeline testing)
- **`AtomizerNXSolver`** — wraps `optimization_engine` NXSolver for real DesigncenterNX 2512 solves
Expression names are exact from binary introspection. Critical: `beam_lenght` has a typo in NX (no 'h') — use exact spelling.
### Outputs extracted from NX:
| Output | Source | Unit | Notes |
|--------|--------|------|-------|
| Mass | Expression `p173``_temp_mass.txt` | kg | Journal extracts after solve |
| Tip displacement | OP2 parse via pyNastran | mm | Max Tz at free end |
| Max VM stress | OP2 parse via pyNastran | MPa | ⚠️ pyNastran returns kPa — divide by 1000 |
### Critical Lessons Learned (2026-02-11)
1. **Path resolution:** Use `Path.resolve()` NOT `Path.absolute()` on Windows. `.absolute()` doesn't resolve `..` components — NX can't follow them.
2. **File references:** NX `.sim` files have absolute internal refs to `.fem`/`.prt`. Never copy model files to iteration folders — solve in-place with backup/restore.
3. **NX version:** Must match exactly. Model files from 2512 won't load in 2412 ("Part file is from a newer version").
4. **pyNastran:** Warns about unsupported NX version 2512 but works fine. Safe to ignore.
## References
- [OPTIMIZATION_STRATEGY.md](./OPTIMIZATION_STRATEGY.md) — Full strategy document
- [../../BREAKDOWN.md](../../BREAKDOWN.md) — Tech Lead's technical analysis
- [../../DECISIONS.md](../../DECISIONS.md) — Decision log
- [../../CONTEXT.md](../../CONTEXT.md) — Project context & expression map
---
*Code: 🏗️ Study Builder (Technical Lead) | Strategy: ⚡ Optimizer Agent | Updated: 2026-02-11*

View File

@@ -0,0 +1,235 @@
# Study: 01_doe_landscape — Hydrotech Beam
> See [../../README.md](../../README.md) for project overview.
## Purpose
Map the design space of the Hydrotech sandwich I-beam to identify feasible regions, characterize variable sensitivities, and prepare data for Phase 2 (TPE optimization). This is Phase 1 of the two-phase strategy (DEC-HB-002).
## Quick Facts
| Item | Value |
|------|-------|
| **Objective** | Minimize mass (kg) |
| **Constraints** | Tip displacement ≤ 10 mm, Von Mises stress ≤ 130 MPa |
| **Design variables** | 4 (3 continuous + 1 integer) |
| **Algorithm** | Phase 1: LHS DoE (50 trials + 1 baseline) |
| **Total budget** | 51 evaluations |
| **Constraint handling** | Deb's feasibility rules (Optuna constraint interface) |
| **Status** | ✅ Pipeline operational — first results obtained 2026-02-11 |
| **Solver** | DesigncenterNX 2512 (NX Nastran SOL 101) |
| **Solve time** | ~12 s/trial (~10 min for full DOE) |
| **First results** | Displacement=17.93mm, Stress=111.9MPa |
## Design Variables (confirmed via NX binary introspection)
| ID | NX Expression | Range | Type | Baseline | Unit |
|----|--------------|-------|------|----------|------|
| DV1 | `beam_half_core_thickness` | 1040 | Continuous | 25.162 | mm |
| DV2 | `beam_face_thickness` | 1040 | Continuous | 21.504 | mm |
| DV3 | `holes_diameter` | 150450 | Continuous | 300 | mm |
| DV4 | `hole_count` | 515 | Integer | 10 | — |
## Usage
### Prerequisites
```bash
pip install -r requirements.txt
```
Requires: Python 3.10+, optuna, scipy, numpy, pandas.
### Development Run (stub solver)
```bash
# From the study directory:
cd projects/hydrotech-beam/studies/01_doe_landscape/
# Run with synthetic results (for pipeline testing):
python run_doe.py --backend stub
# With verbose logging:
python run_doe.py --backend stub -v
# Custom study name:
python run_doe.py --backend stub --study-name my_test_run
```
### Production Run (NXOpen on Windows)
```bash
# On dalidou (Windows node with DesigncenterNX 2512):
cd C:\Users\antoi\Atomizer\projects\hydrotech-beam\studies\01_doe_landscape
conda activate atomizer
python run_doe.py --backend nxopen --model-dir "../../models"
```
> ⚠️ `--model-dir` can be relative — the code resolves it with `Path.resolve()`. NX requires fully resolved paths (no `..` components).
### Resume Interrupted Study
```bash
python run_doe.py --backend stub --resume --study-name hydrotech_beam_doe_phase1
```
### CLI Options
| Flag | Default | Description |
|------|---------|-------------|
| `--backend` | `stub` | `stub` (testing) or `nxopen` (real NX) |
| `--model-dir` | — | Path to NX model files (required for `nxopen`) |
| `--study-name` | `hydrotech_beam_doe_phase1` | Optuna study name |
| `--n-samples` | 50 | Number of LHS sample points |
| `--seed` | 42 | Random seed for reproducibility |
| `--results-dir` | `results/` | Output directory |
| `--resume` | false | Resume existing study |
| `--clean` | false | Delete Optuna DB and start fresh (history DB preserved) |
| `-v` | false | Verbose (DEBUG) logging |
## Files
| File | Description |
|------|-------------|
| `run_doe.py` | Main entry point — orchestrates the DoE study |
| `sampling.py` | LHS generation with stratified integer sampling |
| `geometric_checks.py` | Pre-flight geometric feasibility filter |
| `nx_interface.py` | AtomizerNXSolver wrapping optimization_engine |
| `iteration_manager.py` | Smart retention — last 10 + best 3 with full data |
| `requirements.txt` | Python dependencies |
| `OPTIMIZATION_STRATEGY.md` | Full strategy document |
| `results/` | Output directory (CSV, JSON, Optuna DB) |
| `iterations/` | Per-trial archived outputs |
## Output Files
After a study run, `results/` will contain:
| File | Description |
|------|-------------|
| `doe_results.csv` | All trials — DVs, objectives, constraints, status |
| `doe_summary.json` | Study metadata, statistics, best feasible design |
| `optuna_study.db` | SQLite database (Optuna persistence, resume support) |
| `history.db` | **Persistent** SQLite — all DVs, results, feasibility (survives `--clean`) |
| `history.csv` | CSV mirror of history DB |
### Iteration Folders
Each trial produces `iterations/iterNNN/` containing:
| File | Description |
|------|-------------|
| `params.json` | Design variable values for this trial |
| `params.exp` | NX expression file used for this trial |
| `results.json` | Extracted results (mass, displacement, stress, feasibility) |
| `*.op2` | Nastran binary results (for post-processing) |
| `*.f06` | Nastran text output (for debugging) |
**Smart retention policy** (runs every 5 iterations):
- Keep last 10 iterations with full data
- Keep best 3 feasible iterations with full data
- Strip model files from all others, keep solver outputs
## Architecture
```
run_doe.py
├── sampling.py Generate 50 LHS + 1 baseline
│ └── scipy.stats.qmc.LatinHypercube
├── geometric_checks.py Pre-flight feasibility filter
│ ├── Hole overlap: span/(n-1) - d ≥ 30mm
│ └── Web clearance: 500 - 2·face - d > 0
├── nx_interface.py AtomizerNXSolver (wraps optimization_engine)
│ ├── NXStubSolver → synthetic results (development)
│ └── AtomizerNXSolver → real DesigncenterNX 2512 SOL 101 (production)
├── iteration_manager.py Smart retention (last 10 + best 3)
├── Optuna study
│ ├── SQLite storage (resume support)
│ ├── Enqueued trials (deterministic LHS)
│ └── Deb's feasibility rules (constraint interface)
└── history.db Persistent history (append-only, survives --clean)
```
## Pipeline per Trial
```
Trial N
├── 1. Suggest DVs (from enqueued LHS point)
├── 2. Geometric pre-check
│ ├── FAIL → record as infeasible, skip NX
│ └── PASS ↓
├── 3. Restore master model from backup
├── 4. NX evaluation (IN-PLACE in models/ directory)
│ ├── Write .exp file with trial DVs
│ ├── Open .sim (loads model chain)
│ ├── Import expressions, rebuild geometry
│ ├── Update FEM (remesh)
│ ├── Solve SOL 101 (~12s)
│ └── Extract: mass (p173 → _temp_mass.txt), displacement, stress (via OP2)
├── 5. Archive outputs to iterations/iterNNN/
│ └── params.json, params.exp, results.json, OP2, F06
├── 6. Record results + constraint violations
├── 7. Log to Optuna DB + history.db + CSV
└── 8. Smart retention check (every 5 iterations)
```
## Phase 1 → Phase 2 Gate Criteria
Before proceeding to Phase 2 (TPE optimization), these checks must pass:
| Check | Threshold | Action if FAIL |
|-------|-----------|----------------|
| Feasible points found | ≥ 5 | Expand bounds or relax constraints (escalate to CEO) |
| NX solve success rate | ≥ 80% | Investigate failures, fix model, re-run |
| No systematic crashes at bounds | Visual check | Tighten bounds away from failure region |
## Key Design Decisions
- **Baseline as Trial 0** — LAC lesson: always validate baseline first
- **Pre-flight geometric filter** — catches infeasible geometry before NX (saves compute, avoids crashes)
- **Stratified integer sampling** — ensures all 11 hole_count levels (5-15) are covered
- **Deb's feasibility rules** — no penalty weight tuning; feasible always beats infeasible
- **SQLite persistence** — study can be interrupted and resumed
- **No surrogates** — LAC lesson: direct FEA via TPE beats surrogate + L-BFGS
## NX Integration Notes
**Solver:** DesigncenterNX 2512 (Siemens rebrand of NX). Install: `C:\Program Files\Siemens\DesigncenterNX2512`.
The `nx_interface.py` module provides:
- **`NXStubSolver`** — synthetic results from simplified beam mechanics (for pipeline testing)
- **`AtomizerNXSolver`** — wraps `optimization_engine` NXSolver for real DesigncenterNX 2512 solves
Expression names are exact from binary introspection. Critical: `beam_lenght` has a typo in NX (no 'h') — use exact spelling.
### Outputs extracted from NX:
| Output | Source | Unit | Notes |
|--------|--------|------|-------|
| Mass | Expression `p173``_temp_mass.txt` | kg | Journal extracts after solve |
| Tip displacement | OP2 parse via pyNastran | mm | Max Tz at free end |
| Max VM stress | OP2 parse via pyNastran | MPa | ⚠️ pyNastran returns kPa — divide by 1000 |
### Critical Lessons Learned (2026-02-11)
1. **Path resolution:** Use `Path.resolve()` NOT `Path.absolute()` on Windows. `.absolute()` doesn't resolve `..` components — NX can't follow them.
2. **File references:** NX `.sim` files have absolute internal refs to `.fem`/`.prt`. Never copy model files to iteration folders — solve in-place with backup/restore.
3. **NX version:** Must match exactly. Model files from 2512 won't load in 2412 ("Part file is from a newer version").
4. **pyNastran:** Warns about unsupported NX version 2512 but works fine. Safe to ignore.
## References
- [OPTIMIZATION_STRATEGY.md](./OPTIMIZATION_STRATEGY.md) — Full strategy document
- [../../BREAKDOWN.md](../../BREAKDOWN.md) — Tech Lead's technical analysis
- [../../DECISIONS.md](../../DECISIONS.md) — Decision log
- [../../CONTEXT.md](../../CONTEXT.md) — Project context & expression map
---
*Code: 🏗️ Study Builder (Technical Lead) | Strategy: ⚡ Optimizer Agent | Updated: 2026-02-11*

View File

@@ -1,25 +1,25 @@
# Reference Models — Hydrotech Beam
Golden copies of the NX model files. **Do not modify these directly.**
Studies should copy these to their own `model/` folder and modify there.
## Files
| File | Description | Status |
|------|-------------|--------|
| `Beam.prt` | NX CAD part — sandwich I-beam with lightening holes | ⏳ Pending upload |
| `Beam_fem1.fem` | FEM mesh file | ⏳ Pending upload |
| `Beam_fem1_i.prt` | Idealized part for FEM | ⏳ Pending upload |
| `Beam_sim1.sim` | Simulation file — SOL 101 static | ⏳ Pending upload |
## Key Expression
- `p173` — beam mass (kg)
## Notes
- ⏳ Awaiting Syncthing setup for project folder sync (dalidou ↔ server)
- Model files will appear here once sync is live
- Verify model rebuild across full design variable range before optimization
- **Do NOT modify these reference copies** — studies copy them to their own `model/` folder
# Reference Models — Hydrotech Beam
Golden copies of the NX model files. **Do not modify these directly.**
Studies should copy these to their own `model/` folder and modify there.
## Files
| File | Description | Status |
|------|-------------|--------|
| `Beam.prt` | NX CAD part — sandwich I-beam with lightening holes | ⏳ Pending upload |
| `Beam_fem1.fem` | FEM mesh file | ⏳ Pending upload |
| `Beam_fem1_i.prt` | Idealized part for FEM | ⏳ Pending upload |
| `Beam_sim1.sim` | Simulation file — SOL 101 static | ⏳ Pending upload |
## Key Expression
- `p173` — beam mass (kg)
## Notes
- ⏳ Awaiting Syncthing setup for project folder sync (dalidou ↔ server)
- Model files will appear here once sync is live
- Verify model rebuild across full design variable range before optimization
- **Do NOT modify these reference copies** — studies copy them to their own `model/` folder

View File

@@ -0,0 +1,25 @@
# Reference Models — Hydrotech Beam
Golden copies of the NX model files. **Do not modify these directly.**
Studies should copy these to their own `model/` folder and modify there.
## Files
| File | Description | Status |
|------|-------------|--------|
| `Beam.prt` | NX CAD part — sandwich I-beam with lightening holes | ⏳ Pending upload |
| `Beam_fem1.fem` | FEM mesh file | ⏳ Pending upload |
| `Beam_fem1_i.prt` | Idealized part for FEM | ⏳ Pending upload |
| `Beam_sim1.sim` | Simulation file — SOL 101 static | ⏳ Pending upload |
## Key Expression
- `p173` — beam mass (kg)
## Notes
- ⏳ Awaiting Syncthing setup for project folder sync (dalidou ↔ server)
- Model files will appear here once sync is live
- Verify model rebuild across full design variable range before optimization
- **Do NOT modify these reference copies** — studies copy them to their own `model/` folder

View File

@@ -1,302 +1,302 @@
{
"meta": {
"version": "2.0",
"created": "2026-02-09T12:00:00Z",
"modified": "2026-02-09T12:00:00Z",
"created_by": "optimizer-agent",
"modified_by": "optimizer-agent",
"study_name": "01_doe_landscape",
"description": "Hydrotech Beam — DoE landscape mapping + TPE optimization. Minimize mass subject to displacement and stress constraints.",
"tags": ["hydrotech-beam", "doe", "landscape", "tpe", "single-objective"]
},
"model": {
"sim": {
"path": "models/Beam_sim1.sim",
"solver": "nastran"
},
"part": "models/Beam.prt",
"fem": "models/Beam_fem1.fem",
"idealized": "models/Beam_fem1_i.prt"
},
"design_variables": [
{
"id": "dv_001",
"name": "beam_half_core_thickness",
"expression_name": "beam_half_core_thickness",
"type": "continuous",
"bounds": {
"min": 10,
"max": 40
},
"baseline": 20,
"units": "mm",
"enabled": true,
"description": "Core half-thickness. Stiffness scales ~quadratically via sandwich effect (lever arm). Also adds core mass linearly."
},
{
"id": "dv_002",
"name": "beam_face_thickness",
"expression_name": "beam_face_thickness",
"type": "continuous",
"bounds": {
"min": 10,
"max": 40
},
"baseline": 20,
"units": "mm",
"enabled": true,
"description": "Face sheet thickness. Primary bending stiffness contributor AND primary mass contributor. Key trade-off variable."
},
{
"id": "dv_003",
"name": "holes_diameter",
"expression_name": "holes_diameter",
"type": "continuous",
"bounds": {
"min": 150,
"max": 450
},
"baseline": 300,
"units": "mm",
"enabled": true,
"description": "Lightening hole diameter. Mass reduction scales with d². Stress concentration scales with hole size. Geometric feasibility depends on beam web length."
},
{
"id": "dv_004",
"name": "hole_count",
"expression_name": "hole_count",
"type": "integer",
"bounds": {
"min": 5,
"max": 15
},
"baseline": 10,
"units": "",
"enabled": true,
"description": "Number of lightening holes in the web. Integer variable (11 levels). Interacts strongly with holes_diameter for geometric feasibility."
}
],
"extractors": [
{
"id": "ext_001",
"name": "Mass Extractor",
"type": "expression",
"config": {
"expression_name": "p173",
"description": "Total beam mass from NX expression"
},
"outputs": [
{
"name": "mass",
"metric": "total",
"units": "kg"
}
]
},
{
"id": "ext_002",
"name": "Displacement Extractor",
"type": "displacement",
"config": {
"method": "result_sensor_or_op2",
"component": "magnitude",
"location": "beam_tip",
"description": "Tip displacement magnitude from SOL 101. Preferred: NX result sensor. Fallback: parse .op2 at tip node ID.",
"TODO": "Confirm displacement sensor exists in Beam_sim1.sim OR identify tip node ID"
},
"outputs": [
{
"name": "tip_displacement",
"metric": "max_magnitude",
"units": "mm"
}
]
},
{
"id": "ext_003",
"name": "Stress Extractor",
"type": "stress",
"config": {
"method": "op2_elemental_nodal",
"stress_type": "von_mises",
"scope": "all_elements",
"unit_conversion": "kPa_to_MPa",
"description": "Max von Mises stress (elemental nodal) from SOL 101. pyNastran returns kPa for NX kg-mm-s units — divide by 1000 for MPa.",
"TODO": "Verify element types in model (CQUAD4? CTETRA? CHEXA?) and confirm stress scope"
},
"outputs": [
{
"name": "max_von_mises",
"metric": "max",
"units": "MPa"
}
]
}
],
"objectives": [
{
"id": "obj_001",
"name": "Minimize beam mass",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"units": "kg"
}
],
"constraints": [
{
"id": "con_001",
"name": "Tip displacement limit",
"type": "hard",
"operator": "<=",
"threshold": 10.0,
"source": {
"extractor_id": "ext_002",
"output_name": "tip_displacement"
},
"penalty_config": {
"method": "deb_rules",
"description": "Deb's feasibility rules: feasible always beats infeasible; among infeasible, lower violation wins"
},
"margin_buffer": {
"optimizer_target": 9.5,
"hard_limit": 10.0,
"description": "5% inner margin during optimization to account for numerical noise"
}
},
{
"id": "con_002",
"name": "Von Mises stress limit",
"type": "hard",
"operator": "<=",
"threshold": 130.0,
"source": {
"extractor_id": "ext_003",
"output_name": "max_von_mises"
},
"penalty_config": {
"method": "deb_rules",
"description": "Deb's feasibility rules: feasible always beats infeasible; among infeasible, lower violation wins"
},
"margin_buffer": {
"optimizer_target": 123.5,
"hard_limit": 130.0,
"description": "5% inner margin during optimization to account for mesh sensitivity"
}
}
],
"optimization": {
"phases": [
{
"id": "phase_1",
"name": "DoE Landscape Mapping",
"algorithm": {
"type": "LHS",
"config": {
"criterion": "maximin",
"integer_handling": "round_nearest_stratified",
"seed": 42
}
},
"budget": {
"max_trials": 50,
"baseline_enqueued": true,
"total_with_baseline": 51
},
"purpose": "Map design space, identify feasible region, assess sensitivities and interactions"
},
{
"id": "phase_2",
"name": "TPE Optimization",
"algorithm": {
"type": "TPE",
"config": {
"sampler": "TPESampler",
"n_startup_trials": 0,
"warm_start_from": "phase_1_best_feasible",
"seed": 42,
"constraint_handling": "deb_feasibility_rules"
}
},
"budget": {
"min_trials": 60,
"max_trials": 100,
"adaptive": true
},
"convergence": {
"stall_window": 20,
"stall_threshold_pct": 1.0,
"min_trials_before_check": 60
},
"purpose": "Directed optimization to converge on minimum-mass feasible design"
}
],
"total_budget": {
"min": 114,
"max": 156,
"estimated_hours": "4-5"
},
"baseline_trial": {
"enqueue": true,
"values": {
"beam_half_core_thickness": 20,
"beam_face_thickness": 20,
"holes_diameter": 300,
"hole_count": 10
},
"expected_results": {
"mass_kg": 974,
"tip_displacement_mm": 22,
"max_von_mises_mpa": "UNKNOWN — must measure"
}
}
},
"error_handling": {
"trial_timeout_seconds": 600,
"on_nx_rebuild_failure": "record_infeasible_max_violation",
"on_solver_failure": "record_infeasible_max_violation",
"on_extraction_failure": "record_infeasible_max_violation",
"max_consecutive_failures": 5,
"max_failure_rate_pct": 30,
"nx_process_management": "NEVER kill NX directly. Use NXSessionManager.close_nx_if_allowed() only."
},
"pre_flight_checks": [
{
"id": "pf_001",
"name": "Baseline validation",
"description": "Run Trial 0 with baseline parameters, verify mass ≈ 974 kg and displacement ≈ 22 mm"
},
{
"id": "pf_002",
"name": "Corner tests",
"description": "Test 16 corner combinations (min/max for each DV). Verify NX rebuilds and solves at all corners."
},
{
"id": "pf_003",
"name": "Mesh convergence",
"description": "Run baseline at 3 mesh densities. Verify stress convergence within 10%."
},
{
"id": "pf_004",
"name": "Extractor validation",
"description": "Confirm mass, displacement, and stress extractors return correct values at baseline."
},
{
"id": "pf_005",
"name": "Geometric feasibility",
"description": "Determine beam web length. Verify max(hole_count) × max(holes_diameter) fits. Add ligament constraint if needed."
}
],
"open_items": [
"Beam web length needed for geometric feasibility validation (holes_diameter × hole_count vs available length)",
"Displacement extraction method: result sensor in .sim or .op2 node ID parsing?",
"Stress extraction scope: whole model or specific element group?",
"Verify NX expression name 'p173' maps to mass",
"Benchmark single SOL 101 runtime to refine compute estimates",
"Confirm baseline stress value (currently unknown)",
"Clarify relationship between core/face thickness DVs and web height where holes are placed"
]
}
{
"meta": {
"version": "2.0",
"created": "2026-02-09T12:00:00Z",
"modified": "2026-02-09T12:00:00Z",
"created_by": "optimizer-agent",
"modified_by": "optimizer-agent",
"study_name": "01_doe_landscape",
"description": "Hydrotech Beam — DoE landscape mapping + TPE optimization. Minimize mass subject to displacement and stress constraints.",
"tags": ["hydrotech-beam", "doe", "landscape", "tpe", "single-objective"]
},
"model": {
"sim": {
"path": "models/Beam_sim1.sim",
"solver": "nastran"
},
"part": "models/Beam.prt",
"fem": "models/Beam_fem1.fem",
"idealized": "models/Beam_fem1_i.prt"
},
"design_variables": [
{
"id": "dv_001",
"name": "beam_half_core_thickness",
"expression_name": "beam_half_core_thickness",
"type": "continuous",
"bounds": {
"min": 10,
"max": 40
},
"baseline": 20,
"units": "mm",
"enabled": true,
"description": "Core half-thickness. Stiffness scales ~quadratically via sandwich effect (lever arm). Also adds core mass linearly."
},
{
"id": "dv_002",
"name": "beam_face_thickness",
"expression_name": "beam_face_thickness",
"type": "continuous",
"bounds": {
"min": 10,
"max": 40
},
"baseline": 20,
"units": "mm",
"enabled": true,
"description": "Face sheet thickness. Primary bending stiffness contributor AND primary mass contributor. Key trade-off variable."
},
{
"id": "dv_003",
"name": "holes_diameter",
"expression_name": "holes_diameter",
"type": "continuous",
"bounds": {
"min": 150,
"max": 450
},
"baseline": 300,
"units": "mm",
"enabled": true,
"description": "Lightening hole diameter. Mass reduction scales with d². Stress concentration scales with hole size. Geometric feasibility depends on beam web length."
},
{
"id": "dv_004",
"name": "hole_count",
"expression_name": "hole_count",
"type": "integer",
"bounds": {
"min": 5,
"max": 15
},
"baseline": 10,
"units": "",
"enabled": true,
"description": "Number of lightening holes in the web. Integer variable (11 levels). Interacts strongly with holes_diameter for geometric feasibility."
}
],
"extractors": [
{
"id": "ext_001",
"name": "Mass Extractor",
"type": "expression",
"config": {
"expression_name": "p173",
"description": "Total beam mass from NX expression"
},
"outputs": [
{
"name": "mass",
"metric": "total",
"units": "kg"
}
]
},
{
"id": "ext_002",
"name": "Displacement Extractor",
"type": "displacement",
"config": {
"method": "result_sensor_or_op2",
"component": "magnitude",
"location": "beam_tip",
"description": "Tip displacement magnitude from SOL 101. Preferred: NX result sensor. Fallback: parse .op2 at tip node ID.",
"TODO": "Confirm displacement sensor exists in Beam_sim1.sim OR identify tip node ID"
},
"outputs": [
{
"name": "tip_displacement",
"metric": "max_magnitude",
"units": "mm"
}
]
},
{
"id": "ext_003",
"name": "Stress Extractor",
"type": "stress",
"config": {
"method": "op2_elemental_nodal",
"stress_type": "von_mises",
"scope": "all_elements",
"unit_conversion": "kPa_to_MPa",
"description": "Max von Mises stress (elemental nodal) from SOL 101. pyNastran returns kPa for NX kg-mm-s units — divide by 1000 for MPa.",
"TODO": "Verify element types in model (CQUAD4? CTETRA? CHEXA?) and confirm stress scope"
},
"outputs": [
{
"name": "max_von_mises",
"metric": "max",
"units": "MPa"
}
]
}
],
"objectives": [
{
"id": "obj_001",
"name": "Minimize beam mass",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"units": "kg"
}
],
"constraints": [
{
"id": "con_001",
"name": "Tip displacement limit",
"type": "hard",
"operator": "<=",
"threshold": 10.0,
"source": {
"extractor_id": "ext_002",
"output_name": "tip_displacement"
},
"penalty_config": {
"method": "deb_rules",
"description": "Deb's feasibility rules: feasible always beats infeasible; among infeasible, lower violation wins"
},
"margin_buffer": {
"optimizer_target": 9.5,
"hard_limit": 10.0,
"description": "5% inner margin during optimization to account for numerical noise"
}
},
{
"id": "con_002",
"name": "Von Mises stress limit",
"type": "hard",
"operator": "<=",
"threshold": 130.0,
"source": {
"extractor_id": "ext_003",
"output_name": "max_von_mises"
},
"penalty_config": {
"method": "deb_rules",
"description": "Deb's feasibility rules: feasible always beats infeasible; among infeasible, lower violation wins"
},
"margin_buffer": {
"optimizer_target": 123.5,
"hard_limit": 130.0,
"description": "5% inner margin during optimization to account for mesh sensitivity"
}
}
],
"optimization": {
"phases": [
{
"id": "phase_1",
"name": "DoE Landscape Mapping",
"algorithm": {
"type": "LHS",
"config": {
"criterion": "maximin",
"integer_handling": "round_nearest_stratified",
"seed": 42
}
},
"budget": {
"max_trials": 50,
"baseline_enqueued": true,
"total_with_baseline": 51
},
"purpose": "Map design space, identify feasible region, assess sensitivities and interactions"
},
{
"id": "phase_2",
"name": "TPE Optimization",
"algorithm": {
"type": "TPE",
"config": {
"sampler": "TPESampler",
"n_startup_trials": 0,
"warm_start_from": "phase_1_best_feasible",
"seed": 42,
"constraint_handling": "deb_feasibility_rules"
}
},
"budget": {
"min_trials": 60,
"max_trials": 100,
"adaptive": true
},
"convergence": {
"stall_window": 20,
"stall_threshold_pct": 1.0,
"min_trials_before_check": 60
},
"purpose": "Directed optimization to converge on minimum-mass feasible design"
}
],
"total_budget": {
"min": 114,
"max": 156,
"estimated_hours": "4-5"
},
"baseline_trial": {
"enqueue": true,
"values": {
"beam_half_core_thickness": 20,
"beam_face_thickness": 20,
"holes_diameter": 300,
"hole_count": 10
},
"expected_results": {
"mass_kg": 974,
"tip_displacement_mm": 22,
"max_von_mises_mpa": "UNKNOWN — must measure"
}
}
},
"error_handling": {
"trial_timeout_seconds": 600,
"on_nx_rebuild_failure": "record_infeasible_max_violation",
"on_solver_failure": "record_infeasible_max_violation",
"on_extraction_failure": "record_infeasible_max_violation",
"max_consecutive_failures": 5,
"max_failure_rate_pct": 30,
"nx_process_management": "NEVER kill NX directly. Use NXSessionManager.close_nx_if_allowed() only."
},
"pre_flight_checks": [
{
"id": "pf_001",
"name": "Baseline validation",
"description": "Run Trial 0 with baseline parameters, verify mass ≈ 974 kg and displacement ≈ 22 mm"
},
{
"id": "pf_002",
"name": "Corner tests",
"description": "Test 16 corner combinations (min/max for each DV). Verify NX rebuilds and solves at all corners."
},
{
"id": "pf_003",
"name": "Mesh convergence",
"description": "Run baseline at 3 mesh densities. Verify stress convergence within 10%."
},
{
"id": "pf_004",
"name": "Extractor validation",
"description": "Confirm mass, displacement, and stress extractors return correct values at baseline."
},
{
"id": "pf_005",
"name": "Geometric feasibility",
"description": "Determine beam web length. Verify max(hole_count) × max(holes_diameter) fits. Add ligament constraint if needed."
}
],
"open_items": [
"Beam web length needed for geometric feasibility validation (holes_diameter × hole_count vs available length)",
"Displacement extraction method: result sensor in .sim or .op2 node ID parsing?",
"Stress extraction scope: whole model or specific element group?",
"Verify NX expression name 'p173' maps to mass",
"Benchmark single SOL 101 runtime to refine compute estimates",
"Confirm baseline stress value (currently unknown)",
"Clarify relationship between core/face thickness DVs and web height where holes are placed"
]
}

View File

@@ -0,0 +1,302 @@
{
"meta": {
"version": "2.0",
"created": "2026-02-09T12:00:00Z",
"modified": "2026-02-09T12:00:00Z",
"created_by": "optimizer-agent",
"modified_by": "optimizer-agent",
"study_name": "01_doe_landscape",
"description": "Hydrotech Beam — DoE landscape mapping + TPE optimization. Minimize mass subject to displacement and stress constraints.",
"tags": ["hydrotech-beam", "doe", "landscape", "tpe", "single-objective"]
},
"model": {
"sim": {
"path": "models/Beam_sim1.sim",
"solver": "nastran"
},
"part": "models/Beam.prt",
"fem": "models/Beam_fem1.fem",
"idealized": "models/Beam_fem1_i.prt"
},
"design_variables": [
{
"id": "dv_001",
"name": "beam_half_core_thickness",
"expression_name": "beam_half_core_thickness",
"type": "continuous",
"bounds": {
"min": 10,
"max": 40
},
"baseline": 20,
"units": "mm",
"enabled": true,
"description": "Core half-thickness. Stiffness scales ~quadratically via sandwich effect (lever arm). Also adds core mass linearly."
},
{
"id": "dv_002",
"name": "beam_face_thickness",
"expression_name": "beam_face_thickness",
"type": "continuous",
"bounds": {
"min": 10,
"max": 40
},
"baseline": 20,
"units": "mm",
"enabled": true,
"description": "Face sheet thickness. Primary bending stiffness contributor AND primary mass contributor. Key trade-off variable."
},
{
"id": "dv_003",
"name": "holes_diameter",
"expression_name": "holes_diameter",
"type": "continuous",
"bounds": {
"min": 150,
"max": 450
},
"baseline": 300,
"units": "mm",
"enabled": true,
"description": "Lightening hole diameter. Mass reduction scales with d². Stress concentration scales with hole size. Geometric feasibility depends on beam web length."
},
{
"id": "dv_004",
"name": "hole_count",
"expression_name": "hole_count",
"type": "integer",
"bounds": {
"min": 5,
"max": 15
},
"baseline": 10,
"units": "",
"enabled": true,
"description": "Number of lightening holes in the web. Integer variable (11 levels). Interacts strongly with holes_diameter for geometric feasibility."
}
],
"extractors": [
{
"id": "ext_001",
"name": "Mass Extractor",
"type": "expression",
"config": {
"expression_name": "p173",
"description": "Total beam mass from NX expression"
},
"outputs": [
{
"name": "mass",
"metric": "total",
"units": "kg"
}
]
},
{
"id": "ext_002",
"name": "Displacement Extractor",
"type": "displacement",
"config": {
"method": "result_sensor_or_op2",
"component": "magnitude",
"location": "beam_tip",
"description": "Tip displacement magnitude from SOL 101. Preferred: NX result sensor. Fallback: parse .op2 at tip node ID.",
"TODO": "Confirm displacement sensor exists in Beam_sim1.sim OR identify tip node ID"
},
"outputs": [
{
"name": "tip_displacement",
"metric": "max_magnitude",
"units": "mm"
}
]
},
{
"id": "ext_003",
"name": "Stress Extractor",
"type": "stress",
"config": {
"method": "op2_elemental_nodal",
"stress_type": "von_mises",
"scope": "all_elements",
"unit_conversion": "kPa_to_MPa",
"description": "Max von Mises stress (elemental nodal) from SOL 101. pyNastran returns kPa for NX kg-mm-s units — divide by 1000 for MPa.",
"TODO": "Verify element types in model (CQUAD4? CTETRA? CHEXA?) and confirm stress scope"
},
"outputs": [
{
"name": "max_von_mises",
"metric": "max",
"units": "MPa"
}
]
}
],
"objectives": [
{
"id": "obj_001",
"name": "Minimize beam mass",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_001",
"output_name": "mass"
},
"units": "kg"
}
],
"constraints": [
{
"id": "con_001",
"name": "Tip displacement limit",
"type": "hard",
"operator": "<=",
"threshold": 10.0,
"source": {
"extractor_id": "ext_002",
"output_name": "tip_displacement"
},
"penalty_config": {
"method": "deb_rules",
"description": "Deb's feasibility rules: feasible always beats infeasible; among infeasible, lower violation wins"
},
"margin_buffer": {
"optimizer_target": 9.5,
"hard_limit": 10.0,
"description": "5% inner margin during optimization to account for numerical noise"
}
},
{
"id": "con_002",
"name": "Von Mises stress limit",
"type": "hard",
"operator": "<=",
"threshold": 130.0,
"source": {
"extractor_id": "ext_003",
"output_name": "max_von_mises"
},
"penalty_config": {
"method": "deb_rules",
"description": "Deb's feasibility rules: feasible always beats infeasible; among infeasible, lower violation wins"
},
"margin_buffer": {
"optimizer_target": 123.5,
"hard_limit": 130.0,
"description": "5% inner margin during optimization to account for mesh sensitivity"
}
}
],
"optimization": {
"phases": [
{
"id": "phase_1",
"name": "DoE Landscape Mapping",
"algorithm": {
"type": "LHS",
"config": {
"criterion": "maximin",
"integer_handling": "round_nearest_stratified",
"seed": 42
}
},
"budget": {
"max_trials": 50,
"baseline_enqueued": true,
"total_with_baseline": 51
},
"purpose": "Map design space, identify feasible region, assess sensitivities and interactions"
},
{
"id": "phase_2",
"name": "TPE Optimization",
"algorithm": {
"type": "TPE",
"config": {
"sampler": "TPESampler",
"n_startup_trials": 0,
"warm_start_from": "phase_1_best_feasible",
"seed": 42,
"constraint_handling": "deb_feasibility_rules"
}
},
"budget": {
"min_trials": 60,
"max_trials": 100,
"adaptive": true
},
"convergence": {
"stall_window": 20,
"stall_threshold_pct": 1.0,
"min_trials_before_check": 60
},
"purpose": "Directed optimization to converge on minimum-mass feasible design"
}
],
"total_budget": {
"min": 114,
"max": 156,
"estimated_hours": "4-5"
},
"baseline_trial": {
"enqueue": true,
"values": {
"beam_half_core_thickness": 20,
"beam_face_thickness": 20,
"holes_diameter": 300,
"hole_count": 10
},
"expected_results": {
"mass_kg": 974,
"tip_displacement_mm": 22,
"max_von_mises_mpa": "UNKNOWN — must measure"
}
}
},
"error_handling": {
"trial_timeout_seconds": 600,
"on_nx_rebuild_failure": "record_infeasible_max_violation",
"on_solver_failure": "record_infeasible_max_violation",
"on_extraction_failure": "record_infeasible_max_violation",
"max_consecutive_failures": 5,
"max_failure_rate_pct": 30,
"nx_process_management": "NEVER kill NX directly. Use NXSessionManager.close_nx_if_allowed() only."
},
"pre_flight_checks": [
{
"id": "pf_001",
"name": "Baseline validation",
"description": "Run Trial 0 with baseline parameters, verify mass ≈ 974 kg and displacement ≈ 22 mm"
},
{
"id": "pf_002",
"name": "Corner tests",
"description": "Test 16 corner combinations (min/max for each DV). Verify NX rebuilds and solves at all corners."
},
{
"id": "pf_003",
"name": "Mesh convergence",
"description": "Run baseline at 3 mesh densities. Verify stress convergence within 10%."
},
{
"id": "pf_004",
"name": "Extractor validation",
"description": "Confirm mass, displacement, and stress extractors return correct values at baseline."
},
{
"id": "pf_005",
"name": "Geometric feasibility",
"description": "Determine beam web length. Verify max(hole_count) × max(holes_diameter) fits. Add ligament constraint if needed."
}
],
"open_items": [
"Beam web length needed for geometric feasibility validation (holes_diameter × hole_count vs available length)",
"Displacement extraction method: result sensor in .sim or .op2 node ID parsing?",
"Stress extraction scope: whole model or specific element group?",
"Verify NX expression name 'p173' maps to mass",
"Benchmark single SOL 101 runtime to refine compute estimates",
"Confirm baseline stress value (currently unknown)",
"Clarify relationship between core/face thickness DVs and web height where holes are placed"
]
}

View File

@@ -1,205 +1,205 @@
"""Geometric feasibility pre-flight checks for Hydrotech Beam.
Validates trial design variable combinations against physical geometry
constraints BEFORE sending to NX. Catches infeasible combos that would
cause NX rebuild failures or physically impossible geometries.
References:
OPTIMIZATION_STRATEGY.md §4.2 — Hole overlap analysis
Auditor review 2026-02-10 — Corrected spacing formula
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import NamedTuple
# ---------------------------------------------------------------------------
# Fixed geometry constants (from NX introspection — CONTEXT.md)
# ---------------------------------------------------------------------------
BEAM_HALF_HEIGHT: float = 250.0 # mm — fixed, expression `beam_half_height`
HOLE_SPAN: float = 4000.0 # mm — expression `p6`, total distribution length
MIN_LIGAMENT: float = 30.0 # mm — minimum material between holes (mesh + structural)
class FeasibilityResult(NamedTuple):
"""Result of a geometric feasibility check."""
feasible: bool
reason: str
ligament: float # mm — material between adjacent holes
web_clearance: float # mm — clearance between hole edge and faces
@dataclass(frozen=True)
class DesignPoint:
"""A single design variable combination."""
beam_half_core_thickness: float # mm — DV1
beam_face_thickness: float # mm — DV2
holes_diameter: float # mm — DV3
hole_count: int # — DV4
def compute_ligament(holes_diameter: float, hole_count: int) -> float:
"""Compute ligament width (material between adjacent holes).
The NX pattern places `n` holes across `hole_span` mm using `n-1`
intervals (holes at both endpoints of the span).
Formula (corrected per auditor review):
spacing = hole_span / (hole_count - 1)
ligament = spacing - holes_diameter
Args:
holes_diameter: Hole diameter in mm.
hole_count: Number of holes (integer ≥ 2).
Returns:
Ligament width in mm. Negative means overlap.
"""
if hole_count < 2:
# Single hole — no overlap possible, return large ligament
return HOLE_SPAN
spacing = HOLE_SPAN / (hole_count - 1)
return spacing - holes_diameter
def compute_web_clearance(
beam_face_thickness: float, holes_diameter: float
) -> float:
"""Compute clearance between hole edge and face sheets.
Total beam height = 2 × beam_half_height = 500 mm (fixed).
Web clear height = total_height - 2 × face_thickness.
Clearance = web_clear_height - holes_diameter.
Args:
beam_face_thickness: Face sheet thickness in mm.
holes_diameter: Hole diameter in mm.
Returns:
Web clearance in mm. ≤ 0 means hole doesn't fit.
"""
total_height = 2.0 * BEAM_HALF_HEIGHT # 500 mm
web_clear_height = total_height - 2.0 * beam_face_thickness
return web_clear_height - holes_diameter
def check_feasibility(point: DesignPoint) -> FeasibilityResult:
"""Run all geometric feasibility checks on a design point.
Checks (in order):
1. Hole overlap — ligament between adjacent holes ≥ MIN_LIGAMENT
2. Web clearance — hole fits within the web (between face sheets)
Args:
point: Design variable combination to check.
Returns:
FeasibilityResult with feasibility status and diagnostic info.
"""
ligament = compute_ligament(point.holes_diameter, point.hole_count)
web_clearance = compute_web_clearance(
point.beam_face_thickness, point.holes_diameter
)
# Check 1: Hole overlap
if ligament < MIN_LIGAMENT:
return FeasibilityResult(
feasible=False,
reason=(
f"Hole overlap: ligament={ligament:.1f}mm < "
f"{MIN_LIGAMENT}mm minimum "
f"(d={point.holes_diameter}mm, n={point.hole_count})"
),
ligament=ligament,
web_clearance=web_clearance,
)
# Check 2: Web clearance
if web_clearance <= 0:
return FeasibilityResult(
feasible=False,
reason=(
f"Hole exceeds web: clearance={web_clearance:.1f}mm ≤ 0 "
f"(face={point.beam_face_thickness}mm, "
f"d={point.holes_diameter}mm)"
),
ligament=ligament,
web_clearance=web_clearance,
)
return FeasibilityResult(
feasible=True,
reason="OK",
ligament=ligament,
web_clearance=web_clearance,
)
def check_feasibility_from_values(
beam_half_core_thickness: float,
beam_face_thickness: float,
holes_diameter: float,
hole_count: int,
) -> FeasibilityResult:
"""Convenience wrapper — check feasibility from raw DV values.
Args:
beam_half_core_thickness: Core half-thickness in mm (DV1).
beam_face_thickness: Face sheet thickness in mm (DV2).
holes_diameter: Hole diameter in mm (DV3).
hole_count: Number of holes (DV4).
Returns:
FeasibilityResult with feasibility status and diagnostic info.
"""
point = DesignPoint(
beam_half_core_thickness=beam_half_core_thickness,
beam_face_thickness=beam_face_thickness,
holes_diameter=holes_diameter,
hole_count=hole_count,
)
return check_feasibility(point)
# ---------------------------------------------------------------------------
# Quick self-test
# ---------------------------------------------------------------------------
if __name__ == "__main__":
# Baseline: should be feasible
baseline = DesignPoint(
beam_half_core_thickness=25.162,
beam_face_thickness=21.504,
holes_diameter=300.0,
hole_count=10,
)
result = check_feasibility(baseline)
print(f"Baseline: {result}")
assert result.feasible, f"Baseline should be feasible! Got: {result}"
# Worst case: n=15, d=450 — should be infeasible (overlap)
worst_overlap = DesignPoint(
beam_half_core_thickness=25.0,
beam_face_thickness=20.0,
holes_diameter=450.0,
hole_count=15,
)
result = check_feasibility(worst_overlap)
print(f"Worst overlap: {result}")
assert not result.feasible, "n=15, d=450 should be infeasible"
# Web clearance fail: face=40, d=450
web_fail = DesignPoint(
beam_half_core_thickness=25.0,
beam_face_thickness=40.0,
holes_diameter=450.0,
hole_count=5,
)
result = check_feasibility(web_fail)
print(f"Web clearance fail: {result}")
assert not result.feasible, "face=40, d=450 should fail web clearance"
print("\nAll self-tests passed ✓")
"""Geometric feasibility pre-flight checks for Hydrotech Beam.
Validates trial design variable combinations against physical geometry
constraints BEFORE sending to NX. Catches infeasible combos that would
cause NX rebuild failures or physically impossible geometries.
References:
OPTIMIZATION_STRATEGY.md §4.2 — Hole overlap analysis
Auditor review 2026-02-10 — Corrected spacing formula
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import NamedTuple
# ---------------------------------------------------------------------------
# Fixed geometry constants (from NX introspection — CONTEXT.md)
# ---------------------------------------------------------------------------
BEAM_HALF_HEIGHT: float = 250.0 # mm — fixed, expression `beam_half_height`
HOLE_SPAN: float = 4000.0 # mm — expression `p6`, total distribution length
MIN_LIGAMENT: float = 30.0 # mm — minimum material between holes (mesh + structural)
class FeasibilityResult(NamedTuple):
"""Result of a geometric feasibility check."""
feasible: bool
reason: str
ligament: float # mm — material between adjacent holes
web_clearance: float # mm — clearance between hole edge and faces
@dataclass(frozen=True)
class DesignPoint:
"""A single design variable combination."""
beam_half_core_thickness: float # mm — DV1
beam_face_thickness: float # mm — DV2
holes_diameter: float # mm — DV3
hole_count: int # — DV4
def compute_ligament(holes_diameter: float, hole_count: int) -> float:
"""Compute ligament width (material between adjacent holes).
The NX pattern places `n` holes across `hole_span` mm using `n-1`
intervals (holes at both endpoints of the span).
Formula (corrected per auditor review):
spacing = hole_span / (hole_count - 1)
ligament = spacing - holes_diameter
Args:
holes_diameter: Hole diameter in mm.
hole_count: Number of holes (integer ≥ 2).
Returns:
Ligament width in mm. Negative means overlap.
"""
if hole_count < 2:
# Single hole — no overlap possible, return large ligament
return HOLE_SPAN
spacing = HOLE_SPAN / (hole_count - 1)
return spacing - holes_diameter
def compute_web_clearance(
beam_face_thickness: float, holes_diameter: float
) -> float:
"""Compute clearance between hole edge and face sheets.
Total beam height = 2 × beam_half_height = 500 mm (fixed).
Web clear height = total_height - 2 × face_thickness.
Clearance = web_clear_height - holes_diameter.
Args:
beam_face_thickness: Face sheet thickness in mm.
holes_diameter: Hole diameter in mm.
Returns:
Web clearance in mm. ≤ 0 means hole doesn't fit.
"""
total_height = 2.0 * BEAM_HALF_HEIGHT # 500 mm
web_clear_height = total_height - 2.0 * beam_face_thickness
return web_clear_height - holes_diameter
def check_feasibility(point: DesignPoint) -> FeasibilityResult:
"""Run all geometric feasibility checks on a design point.
Checks (in order):
1. Hole overlap — ligament between adjacent holes ≥ MIN_LIGAMENT
2. Web clearance — hole fits within the web (between face sheets)
Args:
point: Design variable combination to check.
Returns:
FeasibilityResult with feasibility status and diagnostic info.
"""
ligament = compute_ligament(point.holes_diameter, point.hole_count)
web_clearance = compute_web_clearance(
point.beam_face_thickness, point.holes_diameter
)
# Check 1: Hole overlap
if ligament < MIN_LIGAMENT:
return FeasibilityResult(
feasible=False,
reason=(
f"Hole overlap: ligament={ligament:.1f}mm < "
f"{MIN_LIGAMENT}mm minimum "
f"(d={point.holes_diameter}mm, n={point.hole_count})"
),
ligament=ligament,
web_clearance=web_clearance,
)
# Check 2: Web clearance
if web_clearance <= 0:
return FeasibilityResult(
feasible=False,
reason=(
f"Hole exceeds web: clearance={web_clearance:.1f}mm ≤ 0 "
f"(face={point.beam_face_thickness}mm, "
f"d={point.holes_diameter}mm)"
),
ligament=ligament,
web_clearance=web_clearance,
)
return FeasibilityResult(
feasible=True,
reason="OK",
ligament=ligament,
web_clearance=web_clearance,
)
def check_feasibility_from_values(
beam_half_core_thickness: float,
beam_face_thickness: float,
holes_diameter: float,
hole_count: int,
) -> FeasibilityResult:
"""Convenience wrapper — check feasibility from raw DV values.
Args:
beam_half_core_thickness: Core half-thickness in mm (DV1).
beam_face_thickness: Face sheet thickness in mm (DV2).
holes_diameter: Hole diameter in mm (DV3).
hole_count: Number of holes (DV4).
Returns:
FeasibilityResult with feasibility status and diagnostic info.
"""
point = DesignPoint(
beam_half_core_thickness=beam_half_core_thickness,
beam_face_thickness=beam_face_thickness,
holes_diameter=holes_diameter,
hole_count=hole_count,
)
return check_feasibility(point)
# ---------------------------------------------------------------------------
# Quick self-test
# ---------------------------------------------------------------------------
if __name__ == "__main__":
# Baseline: should be feasible
baseline = DesignPoint(
beam_half_core_thickness=25.162,
beam_face_thickness=21.504,
holes_diameter=300.0,
hole_count=10,
)
result = check_feasibility(baseline)
print(f"Baseline: {result}")
assert result.feasible, f"Baseline should be feasible! Got: {result}"
# Worst case: n=15, d=450 — should be infeasible (overlap)
worst_overlap = DesignPoint(
beam_half_core_thickness=25.0,
beam_face_thickness=20.0,
holes_diameter=450.0,
hole_count=15,
)
result = check_feasibility(worst_overlap)
print(f"Worst overlap: {result}")
assert not result.feasible, "n=15, d=450 should be infeasible"
# Web clearance fail: face=40, d=450
web_fail = DesignPoint(
beam_half_core_thickness=25.0,
beam_face_thickness=40.0,
holes_diameter=450.0,
hole_count=5,
)
result = check_feasibility(web_fail)
print(f"Web clearance fail: {result}")
assert not result.feasible, "face=40, d=450 should fail web clearance"
print("\nAll self-tests passed ✓")

View File

@@ -0,0 +1,205 @@
"""Geometric feasibility pre-flight checks for Hydrotech Beam.
Validates trial design variable combinations against physical geometry
constraints BEFORE sending to NX. Catches infeasible combos that would
cause NX rebuild failures or physically impossible geometries.
References:
OPTIMIZATION_STRATEGY.md §4.2 — Hole overlap analysis
Auditor review 2026-02-10 — Corrected spacing formula
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import NamedTuple
# ---------------------------------------------------------------------------
# Fixed geometry constants (from NX introspection — CONTEXT.md)
# ---------------------------------------------------------------------------
BEAM_HALF_HEIGHT: float = 250.0 # mm — fixed, expression `beam_half_height`
HOLE_SPAN: float = 4000.0 # mm — expression `p6`, total distribution length
MIN_LIGAMENT: float = 30.0 # mm — minimum material between holes (mesh + structural)
class FeasibilityResult(NamedTuple):
"""Result of a geometric feasibility check."""
feasible: bool
reason: str
ligament: float # mm — material between adjacent holes
web_clearance: float # mm — clearance between hole edge and faces
@dataclass(frozen=True)
class DesignPoint:
"""A single design variable combination."""
beam_half_core_thickness: float # mm — DV1
beam_face_thickness: float # mm — DV2
holes_diameter: float # mm — DV3
hole_count: int # — DV4
def compute_ligament(holes_diameter: float, hole_count: int) -> float:
"""Compute ligament width (material between adjacent holes).
The NX pattern places `n` holes across `hole_span` mm using `n-1`
intervals (holes at both endpoints of the span).
Formula (corrected per auditor review):
spacing = hole_span / (hole_count - 1)
ligament = spacing - holes_diameter
Args:
holes_diameter: Hole diameter in mm.
hole_count: Number of holes (integer ≥ 2).
Returns:
Ligament width in mm. Negative means overlap.
"""
if hole_count < 2:
# Single hole — no overlap possible, return large ligament
return HOLE_SPAN
spacing = HOLE_SPAN / (hole_count - 1)
return spacing - holes_diameter
def compute_web_clearance(
beam_face_thickness: float, holes_diameter: float
) -> float:
"""Compute clearance between hole edge and face sheets.
Total beam height = 2 × beam_half_height = 500 mm (fixed).
Web clear height = total_height - 2 × face_thickness.
Clearance = web_clear_height - holes_diameter.
Args:
beam_face_thickness: Face sheet thickness in mm.
holes_diameter: Hole diameter in mm.
Returns:
Web clearance in mm. ≤ 0 means hole doesn't fit.
"""
total_height = 2.0 * BEAM_HALF_HEIGHT # 500 mm
web_clear_height = total_height - 2.0 * beam_face_thickness
return web_clear_height - holes_diameter
def check_feasibility(point: DesignPoint) -> FeasibilityResult:
"""Run all geometric feasibility checks on a design point.
Checks (in order):
1. Hole overlap — ligament between adjacent holes ≥ MIN_LIGAMENT
2. Web clearance — hole fits within the web (between face sheets)
Args:
point: Design variable combination to check.
Returns:
FeasibilityResult with feasibility status and diagnostic info.
"""
ligament = compute_ligament(point.holes_diameter, point.hole_count)
web_clearance = compute_web_clearance(
point.beam_face_thickness, point.holes_diameter
)
# Check 1: Hole overlap
if ligament < MIN_LIGAMENT:
return FeasibilityResult(
feasible=False,
reason=(
f"Hole overlap: ligament={ligament:.1f}mm < "
f"{MIN_LIGAMENT}mm minimum "
f"(d={point.holes_diameter}mm, n={point.hole_count})"
),
ligament=ligament,
web_clearance=web_clearance,
)
# Check 2: Web clearance
if web_clearance <= 0:
return FeasibilityResult(
feasible=False,
reason=(
f"Hole exceeds web: clearance={web_clearance:.1f}mm ≤ 0 "
f"(face={point.beam_face_thickness}mm, "
f"d={point.holes_diameter}mm)"
),
ligament=ligament,
web_clearance=web_clearance,
)
return FeasibilityResult(
feasible=True,
reason="OK",
ligament=ligament,
web_clearance=web_clearance,
)
def check_feasibility_from_values(
beam_half_core_thickness: float,
beam_face_thickness: float,
holes_diameter: float,
hole_count: int,
) -> FeasibilityResult:
"""Convenience wrapper — check feasibility from raw DV values.
Args:
beam_half_core_thickness: Core half-thickness in mm (DV1).
beam_face_thickness: Face sheet thickness in mm (DV2).
holes_diameter: Hole diameter in mm (DV3).
hole_count: Number of holes (DV4).
Returns:
FeasibilityResult with feasibility status and diagnostic info.
"""
point = DesignPoint(
beam_half_core_thickness=beam_half_core_thickness,
beam_face_thickness=beam_face_thickness,
holes_diameter=holes_diameter,
hole_count=hole_count,
)
return check_feasibility(point)
# ---------------------------------------------------------------------------
# Quick self-test
# ---------------------------------------------------------------------------
if __name__ == "__main__":
# Baseline: should be feasible
baseline = DesignPoint(
beam_half_core_thickness=25.162,
beam_face_thickness=21.504,
holes_diameter=300.0,
hole_count=10,
)
result = check_feasibility(baseline)
print(f"Baseline: {result}")
assert result.feasible, f"Baseline should be feasible! Got: {result}"
# Worst case: n=15, d=450 — should be infeasible (overlap)
worst_overlap = DesignPoint(
beam_half_core_thickness=25.0,
beam_face_thickness=20.0,
holes_diameter=450.0,
hole_count=15,
)
result = check_feasibility(worst_overlap)
print(f"Worst overlap: {result}")
assert not result.feasible, "n=15, d=450 should be infeasible"
# Web clearance fail: face=40, d=450
web_fail = DesignPoint(
beam_half_core_thickness=25.0,
beam_face_thickness=40.0,
holes_diameter=450.0,
hole_count=5,
)
result = check_feasibility(web_fail)
print(f"Web clearance fail: {result}")
assert not result.feasible, "face=40, d=450 should fail web clearance"
print("\nAll self-tests passed ✓")

View File

@@ -1,235 +1,235 @@
"""Persistent trial history — append-only, survives Optuna resets.
Every trial is logged to `history.db` (SQLite) and exported to `history.csv`.
Never deleted by --clean. Full lineage across all studies and phases.
Usage:
history = TrialHistory(results_dir)
history.log_trial(study_name, trial_id, params, results, ...)
history.export_csv()
df = history.query("SELECT * FROM trials WHERE mass_kg < 100")
"""
from __future__ import annotations
import csv
import json
import logging
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
logger = logging.getLogger(__name__)
# Schema version — bump if columns change
SCHEMA_VERSION = 1
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS trials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
study_name TEXT NOT NULL,
trial_id INTEGER NOT NULL,
iteration TEXT,
timestamp TEXT NOT NULL,
-- Design variables
beam_half_core_thickness REAL,
beam_face_thickness REAL,
holes_diameter REAL,
hole_count INTEGER,
-- Results
mass_kg REAL,
tip_displacement_mm REAL,
max_von_mises_mpa REAL,
-- Constraint checks
disp_feasible INTEGER, -- 0/1
stress_feasible INTEGER, -- 0/1
geo_feasible INTEGER, -- 0/1
fully_feasible INTEGER, -- 0/1
-- Meta
status TEXT DEFAULT 'COMPLETE', -- COMPLETE, FAILED, PRUNED
error_message TEXT,
solve_time_s REAL,
iter_path TEXT,
notes TEXT,
-- Unique constraint: no duplicate (study, trial) pairs
UNIQUE(study_name, trial_id)
);
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY
);
"""
# Constraint thresholds (from OPTIMIZATION_STRATEGY.md)
DISP_LIMIT_MM = 10.0
STRESS_LIMIT_MPA = 130.0
# CSV column order
CSV_COLUMNS = [
"study_name", "trial_id", "iteration", "timestamp",
"beam_half_core_thickness", "beam_face_thickness",
"holes_diameter", "hole_count",
"mass_kg", "tip_displacement_mm", "max_von_mises_mpa",
"disp_feasible", "stress_feasible", "geo_feasible", "fully_feasible",
"status", "error_message", "solve_time_s", "iter_path",
]
class TrialHistory:
"""Append-only trial history database."""
def __init__(self, results_dir: Path | str):
self.results_dir = Path(results_dir)
self.results_dir.mkdir(parents=True, exist_ok=True)
self.db_path = self.results_dir / "history.db"
self.csv_path = self.results_dir / "history.csv"
self._conn = sqlite3.connect(str(self.db_path))
self._conn.row_factory = sqlite3.Row
self._conn.execute("PRAGMA journal_mode=WAL") # safe concurrent reads
self._init_schema()
count = self._conn.execute("SELECT COUNT(*) FROM trials").fetchone()[0]
logger.info("Trial history: %s (%d records)", self.db_path.name, count)
def _init_schema(self) -> None:
"""Create tables if they don't exist."""
self._conn.executescript(CREATE_TABLE)
# Check/set schema version
row = self._conn.execute(
"SELECT version FROM schema_version ORDER BY version DESC LIMIT 1"
).fetchone()
if row is None:
self._conn.execute(
"INSERT INTO schema_version (version) VALUES (?)",
(SCHEMA_VERSION,),
)
self._conn.commit()
def log_trial(
self,
study_name: str,
trial_id: int,
params: dict[str, float],
mass_kg: float = float("nan"),
tip_displacement_mm: float = float("nan"),
max_von_mises_mpa: float = float("nan"),
geo_feasible: bool = True,
status: str = "COMPLETE",
error_message: str | None = None,
solve_time_s: float = 0.0,
iter_path: str | None = None,
notes: str | None = None,
iteration_number: int | None = None,
) -> None:
"""Log a single trial result.
Uses INSERT OR REPLACE so re-runs of the same trial update cleanly.
"""
import math
disp_ok = (
not math.isnan(tip_displacement_mm)
and tip_displacement_mm <= DISP_LIMIT_MM
)
stress_ok = (
not math.isnan(max_von_mises_mpa)
and max_von_mises_mpa <= STRESS_LIMIT_MPA
)
iteration = f"iter{iteration_number:03d}" if iteration_number else None
try:
self._conn.execute(
"""
INSERT OR REPLACE INTO trials (
study_name, trial_id, iteration, timestamp,
beam_half_core_thickness, beam_face_thickness,
holes_diameter, hole_count,
mass_kg, tip_displacement_mm, max_von_mises_mpa,
disp_feasible, stress_feasible, geo_feasible, fully_feasible,
status, error_message, solve_time_s, iter_path, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
study_name,
trial_id,
iteration,
datetime.now(timezone.utc).isoformat(),
params.get("beam_half_core_thickness"),
params.get("beam_face_thickness"),
params.get("holes_diameter"),
params.get("hole_count"),
mass_kg,
tip_displacement_mm,
max_von_mises_mpa,
int(disp_ok),
int(stress_ok),
int(geo_feasible),
int(disp_ok and stress_ok and geo_feasible),
status,
error_message,
solve_time_s,
iter_path,
notes,
),
)
self._conn.commit()
except sqlite3.Error as e:
logger.error("Failed to log trial %d: %s", trial_id, e)
def export_csv(self) -> Path:
"""Export all trials to CSV (overwrite). Returns path."""
rows = self._conn.execute(
f"SELECT {', '.join(CSV_COLUMNS)} FROM trials ORDER BY study_name, trial_id"
).fetchall()
with open(self.csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(CSV_COLUMNS)
for row in rows:
writer.writerow([row[col] for col in CSV_COLUMNS])
logger.info("Exported %d trials to %s", len(rows), self.csv_path.name)
return self.csv_path
def query(self, sql: str, params: tuple = ()) -> list[dict]:
"""Run an arbitrary SELECT query. Returns list of dicts."""
rows = self._conn.execute(sql, params).fetchall()
return [dict(row) for row in rows]
def get_study_summary(self, study_name: str) -> dict[str, Any]:
"""Get summary stats for a study."""
rows = self.query(
"SELECT * FROM trials WHERE study_name = ?", (study_name,)
)
if not rows:
return {"study_name": study_name, "total": 0}
complete = [r for r in rows if r["status"] == "COMPLETE"]
feasible = [r for r in complete if r["fully_feasible"]]
masses = [r["mass_kg"] for r in feasible if r["mass_kg"] is not None]
return {
"study_name": study_name,
"total": len(rows),
"complete": len(complete),
"failed": len(rows) - len(complete),
"feasible": len(feasible),
"best_mass_kg": min(masses) if masses else None,
"solve_rate": len(complete) / len(rows) * 100 if rows else 0,
"feasibility_rate": len(feasible) / len(complete) * 100 if complete else 0,
}
def close(self) -> None:
"""Export CSV and close connection."""
self.export_csv()
self._conn.close()
"""Persistent trial history — append-only, survives Optuna resets.
Every trial is logged to `history.db` (SQLite) and exported to `history.csv`.
Never deleted by --clean. Full lineage across all studies and phases.
Usage:
history = TrialHistory(results_dir)
history.log_trial(study_name, trial_id, params, results, ...)
history.export_csv()
df = history.query("SELECT * FROM trials WHERE mass_kg < 100")
"""
from __future__ import annotations
import csv
import json
import logging
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
logger = logging.getLogger(__name__)
# Schema version — bump if columns change
SCHEMA_VERSION = 1
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS trials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
study_name TEXT NOT NULL,
trial_id INTEGER NOT NULL,
iteration TEXT,
timestamp TEXT NOT NULL,
-- Design variables
beam_half_core_thickness REAL,
beam_face_thickness REAL,
holes_diameter REAL,
hole_count INTEGER,
-- Results
mass_kg REAL,
tip_displacement_mm REAL,
max_von_mises_mpa REAL,
-- Constraint checks
disp_feasible INTEGER, -- 0/1
stress_feasible INTEGER, -- 0/1
geo_feasible INTEGER, -- 0/1
fully_feasible INTEGER, -- 0/1
-- Meta
status TEXT DEFAULT 'COMPLETE', -- COMPLETE, FAILED, PRUNED
error_message TEXT,
solve_time_s REAL,
iter_path TEXT,
notes TEXT,
-- Unique constraint: no duplicate (study, trial) pairs
UNIQUE(study_name, trial_id)
);
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY
);
"""
# Constraint thresholds (from OPTIMIZATION_STRATEGY.md)
DISP_LIMIT_MM = 10.0
STRESS_LIMIT_MPA = 130.0
# CSV column order
CSV_COLUMNS = [
"study_name", "trial_id", "iteration", "timestamp",
"beam_half_core_thickness", "beam_face_thickness",
"holes_diameter", "hole_count",
"mass_kg", "tip_displacement_mm", "max_von_mises_mpa",
"disp_feasible", "stress_feasible", "geo_feasible", "fully_feasible",
"status", "error_message", "solve_time_s", "iter_path",
]
class TrialHistory:
"""Append-only trial history database."""
def __init__(self, results_dir: Path | str):
self.results_dir = Path(results_dir)
self.results_dir.mkdir(parents=True, exist_ok=True)
self.db_path = self.results_dir / "history.db"
self.csv_path = self.results_dir / "history.csv"
self._conn = sqlite3.connect(str(self.db_path))
self._conn.row_factory = sqlite3.Row
self._conn.execute("PRAGMA journal_mode=WAL") # safe concurrent reads
self._init_schema()
count = self._conn.execute("SELECT COUNT(*) FROM trials").fetchone()[0]
logger.info("Trial history: %s (%d records)", self.db_path.name, count)
def _init_schema(self) -> None:
"""Create tables if they don't exist."""
self._conn.executescript(CREATE_TABLE)
# Check/set schema version
row = self._conn.execute(
"SELECT version FROM schema_version ORDER BY version DESC LIMIT 1"
).fetchone()
if row is None:
self._conn.execute(
"INSERT INTO schema_version (version) VALUES (?)",
(SCHEMA_VERSION,),
)
self._conn.commit()
def log_trial(
self,
study_name: str,
trial_id: int,
params: dict[str, float],
mass_kg: float = float("nan"),
tip_displacement_mm: float = float("nan"),
max_von_mises_mpa: float = float("nan"),
geo_feasible: bool = True,
status: str = "COMPLETE",
error_message: str | None = None,
solve_time_s: float = 0.0,
iter_path: str | None = None,
notes: str | None = None,
iteration_number: int | None = None,
) -> None:
"""Log a single trial result.
Uses INSERT OR REPLACE so re-runs of the same trial update cleanly.
"""
import math
disp_ok = (
not math.isnan(tip_displacement_mm)
and tip_displacement_mm <= DISP_LIMIT_MM
)
stress_ok = (
not math.isnan(max_von_mises_mpa)
and max_von_mises_mpa <= STRESS_LIMIT_MPA
)
iteration = f"iter{iteration_number:03d}" if iteration_number else None
try:
self._conn.execute(
"""
INSERT OR REPLACE INTO trials (
study_name, trial_id, iteration, timestamp,
beam_half_core_thickness, beam_face_thickness,
holes_diameter, hole_count,
mass_kg, tip_displacement_mm, max_von_mises_mpa,
disp_feasible, stress_feasible, geo_feasible, fully_feasible,
status, error_message, solve_time_s, iter_path, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
study_name,
trial_id,
iteration,
datetime.now(timezone.utc).isoformat(),
params.get("beam_half_core_thickness"),
params.get("beam_face_thickness"),
params.get("holes_diameter"),
params.get("hole_count"),
mass_kg,
tip_displacement_mm,
max_von_mises_mpa,
int(disp_ok),
int(stress_ok),
int(geo_feasible),
int(disp_ok and stress_ok and geo_feasible),
status,
error_message,
solve_time_s,
iter_path,
notes,
),
)
self._conn.commit()
except sqlite3.Error as e:
logger.error("Failed to log trial %d: %s", trial_id, e)
def export_csv(self) -> Path:
"""Export all trials to CSV (overwrite). Returns path."""
rows = self._conn.execute(
f"SELECT {', '.join(CSV_COLUMNS)} FROM trials ORDER BY study_name, trial_id"
).fetchall()
with open(self.csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(CSV_COLUMNS)
for row in rows:
writer.writerow([row[col] for col in CSV_COLUMNS])
logger.info("Exported %d trials to %s", len(rows), self.csv_path.name)
return self.csv_path
def query(self, sql: str, params: tuple = ()) -> list[dict]:
"""Run an arbitrary SELECT query. Returns list of dicts."""
rows = self._conn.execute(sql, params).fetchall()
return [dict(row) for row in rows]
def get_study_summary(self, study_name: str) -> dict[str, Any]:
"""Get summary stats for a study."""
rows = self.query(
"SELECT * FROM trials WHERE study_name = ?", (study_name,)
)
if not rows:
return {"study_name": study_name, "total": 0}
complete = [r for r in rows if r["status"] == "COMPLETE"]
feasible = [r for r in complete if r["fully_feasible"]]
masses = [r["mass_kg"] for r in feasible if r["mass_kg"] is not None]
return {
"study_name": study_name,
"total": len(rows),
"complete": len(complete),
"failed": len(rows) - len(complete),
"feasible": len(feasible),
"best_mass_kg": min(masses) if masses else None,
"solve_rate": len(complete) / len(rows) * 100 if rows else 0,
"feasibility_rate": len(feasible) / len(complete) * 100 if complete else 0,
}
def close(self) -> None:
"""Export CSV and close connection."""
self.export_csv()
self._conn.close()

View File

@@ -0,0 +1,235 @@
"""Persistent trial history — append-only, survives Optuna resets.
Every trial is logged to `history.db` (SQLite) and exported to `history.csv`.
Never deleted by --clean. Full lineage across all studies and phases.
Usage:
history = TrialHistory(results_dir)
history.log_trial(study_name, trial_id, params, results, ...)
history.export_csv()
df = history.query("SELECT * FROM trials WHERE mass_kg < 100")
"""
from __future__ import annotations
import csv
import json
import logging
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
logger = logging.getLogger(__name__)
# Schema version — bump if columns change
SCHEMA_VERSION = 1
CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS trials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
study_name TEXT NOT NULL,
trial_id INTEGER NOT NULL,
iteration TEXT,
timestamp TEXT NOT NULL,
-- Design variables
beam_half_core_thickness REAL,
beam_face_thickness REAL,
holes_diameter REAL,
hole_count INTEGER,
-- Results
mass_kg REAL,
tip_displacement_mm REAL,
max_von_mises_mpa REAL,
-- Constraint checks
disp_feasible INTEGER, -- 0/1
stress_feasible INTEGER, -- 0/1
geo_feasible INTEGER, -- 0/1
fully_feasible INTEGER, -- 0/1
-- Meta
status TEXT DEFAULT 'COMPLETE', -- COMPLETE, FAILED, PRUNED
error_message TEXT,
solve_time_s REAL,
iter_path TEXT,
notes TEXT,
-- Unique constraint: no duplicate (study, trial) pairs
UNIQUE(study_name, trial_id)
);
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY
);
"""
# Constraint thresholds (from OPTIMIZATION_STRATEGY.md)
DISP_LIMIT_MM = 10.0
STRESS_LIMIT_MPA = 130.0
# CSV column order
CSV_COLUMNS = [
"study_name", "trial_id", "iteration", "timestamp",
"beam_half_core_thickness", "beam_face_thickness",
"holes_diameter", "hole_count",
"mass_kg", "tip_displacement_mm", "max_von_mises_mpa",
"disp_feasible", "stress_feasible", "geo_feasible", "fully_feasible",
"status", "error_message", "solve_time_s", "iter_path",
]
class TrialHistory:
"""Append-only trial history database."""
def __init__(self, results_dir: Path | str):
self.results_dir = Path(results_dir)
self.results_dir.mkdir(parents=True, exist_ok=True)
self.db_path = self.results_dir / "history.db"
self.csv_path = self.results_dir / "history.csv"
self._conn = sqlite3.connect(str(self.db_path))
self._conn.row_factory = sqlite3.Row
self._conn.execute("PRAGMA journal_mode=WAL") # safe concurrent reads
self._init_schema()
count = self._conn.execute("SELECT COUNT(*) FROM trials").fetchone()[0]
logger.info("Trial history: %s (%d records)", self.db_path.name, count)
def _init_schema(self) -> None:
"""Create tables if they don't exist."""
self._conn.executescript(CREATE_TABLE)
# Check/set schema version
row = self._conn.execute(
"SELECT version FROM schema_version ORDER BY version DESC LIMIT 1"
).fetchone()
if row is None:
self._conn.execute(
"INSERT INTO schema_version (version) VALUES (?)",
(SCHEMA_VERSION,),
)
self._conn.commit()
def log_trial(
self,
study_name: str,
trial_id: int,
params: dict[str, float],
mass_kg: float = float("nan"),
tip_displacement_mm: float = float("nan"),
max_von_mises_mpa: float = float("nan"),
geo_feasible: bool = True,
status: str = "COMPLETE",
error_message: str | None = None,
solve_time_s: float = 0.0,
iter_path: str | None = None,
notes: str | None = None,
iteration_number: int | None = None,
) -> None:
"""Log a single trial result.
Uses INSERT OR REPLACE so re-runs of the same trial update cleanly.
"""
import math
disp_ok = (
not math.isnan(tip_displacement_mm)
and tip_displacement_mm <= DISP_LIMIT_MM
)
stress_ok = (
not math.isnan(max_von_mises_mpa)
and max_von_mises_mpa <= STRESS_LIMIT_MPA
)
iteration = f"iter{iteration_number:03d}" if iteration_number else None
try:
self._conn.execute(
"""
INSERT OR REPLACE INTO trials (
study_name, trial_id, iteration, timestamp,
beam_half_core_thickness, beam_face_thickness,
holes_diameter, hole_count,
mass_kg, tip_displacement_mm, max_von_mises_mpa,
disp_feasible, stress_feasible, geo_feasible, fully_feasible,
status, error_message, solve_time_s, iter_path, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
study_name,
trial_id,
iteration,
datetime.now(timezone.utc).isoformat(),
params.get("beam_half_core_thickness"),
params.get("beam_face_thickness"),
params.get("holes_diameter"),
params.get("hole_count"),
mass_kg,
tip_displacement_mm,
max_von_mises_mpa,
int(disp_ok),
int(stress_ok),
int(geo_feasible),
int(disp_ok and stress_ok and geo_feasible),
status,
error_message,
solve_time_s,
iter_path,
notes,
),
)
self._conn.commit()
except sqlite3.Error as e:
logger.error("Failed to log trial %d: %s", trial_id, e)
def export_csv(self) -> Path:
"""Export all trials to CSV (overwrite). Returns path."""
rows = self._conn.execute(
f"SELECT {', '.join(CSV_COLUMNS)} FROM trials ORDER BY study_name, trial_id"
).fetchall()
with open(self.csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(CSV_COLUMNS)
for row in rows:
writer.writerow([row[col] for col in CSV_COLUMNS])
logger.info("Exported %d trials to %s", len(rows), self.csv_path.name)
return self.csv_path
def query(self, sql: str, params: tuple = ()) -> list[dict]:
"""Run an arbitrary SELECT query. Returns list of dicts."""
rows = self._conn.execute(sql, params).fetchall()
return [dict(row) for row in rows]
def get_study_summary(self, study_name: str) -> dict[str, Any]:
"""Get summary stats for a study."""
rows = self.query(
"SELECT * FROM trials WHERE study_name = ?", (study_name,)
)
if not rows:
return {"study_name": study_name, "total": 0}
complete = [r for r in rows if r["status"] == "COMPLETE"]
feasible = [r for r in complete if r["fully_feasible"]]
masses = [r["mass_kg"] for r in feasible if r["mass_kg"] is not None]
return {
"study_name": study_name,
"total": len(rows),
"complete": len(complete),
"failed": len(rows) - len(complete),
"feasible": len(feasible),
"best_mass_kg": min(masses) if masses else None,
"solve_rate": len(complete) / len(rows) * 100 if rows else 0,
"feasibility_rate": len(feasible) / len(complete) * 100 if complete else 0,
}
def close(self) -> None:
"""Export CSV and close connection."""
self.export_csv()
self._conn.close()

View File

@@ -1,249 +1,249 @@
"""Smart iteration folder management for Hydrotech Beam optimization.
Manages iteration folders with intelligent retention:
- Each iteration gets a full copy of model files (openable in NX for debug)
- Last N iterations: keep full model files (rolling window)
- Best K iterations: keep full model files (by objective value)
- All others: strip model files, keep only solver outputs + params
This gives debuggability (open any recent/best iteration in NX) while
keeping disk usage bounded.
References:
CEO design brief (2026-02-11): "all models properly saved in their
iteration folder, keep last 10, keep best 3, delete stacking models"
"""
from __future__ import annotations
import json
import logging
import shutil
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
# NX model file extensions (copied to each iteration)
MODEL_EXTENSIONS = {".prt", ".fem", ".sim"}
# Solver output extensions (always kept, even after stripping)
KEEP_EXTENSIONS = {".op2", ".f06", ".dat", ".log", ".json", ".txt", ".csv"}
# Default retention policy
DEFAULT_KEEP_RECENT = 10 # keep last N iterations with full models
DEFAULT_KEEP_BEST = 3 # keep best K iterations with full models
@dataclass
class IterationInfo:
"""Metadata for a single iteration."""
number: int
path: Path
mass: float = float("inf")
displacement: float = float("inf")
stress: float = float("inf")
feasible: bool = False
has_models: bool = True # False after stripping
@dataclass
class IterationManager:
"""Manages iteration folders with smart retention.
Usage:
mgr = IterationManager(study_dir, master_model_dir)
# Before each trial:
iter_dir = mgr.prepare_iteration(iteration_number)
# After trial completes:
mgr.record_result(iteration_number, mass=..., displacement=..., stress=...)
# Periodically or at study end:
mgr.apply_retention()
"""
study_dir: Path
master_model_dir: Path
keep_recent: int = DEFAULT_KEEP_RECENT
keep_best: int = DEFAULT_KEEP_BEST
_iterations: dict[int, IterationInfo] = field(default_factory=dict, repr=False)
def __post_init__(self) -> None:
self.iterations_dir = self.study_dir / "iterations"
self.iterations_dir.mkdir(parents=True, exist_ok=True)
# Scan existing iterations (for resume support)
for d in sorted(self.iterations_dir.iterdir()):
if d.is_dir() and d.name.startswith("iter"):
try:
num = int(d.name.replace("iter", ""))
info = IterationInfo(number=num, path=d)
# Load results if available
results_file = d / "results.json"
if results_file.exists():
data = json.loads(results_file.read_text())
info.mass = data.get("mass_kg", float("inf"))
info.displacement = data.get("tip_displacement_mm", float("inf"))
info.stress = data.get("max_von_mises_mpa", float("inf"))
info.feasible = (
info.displacement <= 10.0 and info.stress <= 130.0
)
# Check if model files are present
info.has_models = any(
f.suffix in MODEL_EXTENSIONS for f in d.iterdir()
)
self._iterations[num] = info
except (ValueError, json.JSONDecodeError):
continue
if self._iterations:
logger.info(
"Loaded %d existing iterations (resume support)",
len(self._iterations),
)
def prepare_iteration(self, iteration_number: int) -> Path:
"""Set up an iteration folder with fresh model copies.
Copies all model files from master_model_dir to the iteration folder.
All paths are resolved to absolute to avoid NX reference issues.
Args:
iteration_number: Trial number (1-indexed).
Returns:
Absolute path to the iteration folder.
"""
iter_dir = (self.iterations_dir / f"iter{iteration_number:03d}").resolve()
# Clean up if exists (failed previous run)
if iter_dir.exists():
shutil.rmtree(iter_dir)
iter_dir.mkdir(parents=True)
# Copy ALL model files (so NX can resolve references within the folder)
master = self.master_model_dir.resolve()
copied = 0
for ext in MODEL_EXTENSIONS:
for src in master.glob(f"*{ext}"):
shutil.copy2(src, iter_dir / src.name)
copied += 1
logger.info(
"Prepared iter%03d: copied %d model files to %s",
iteration_number, copied, iter_dir,
)
# Track iteration
self._iterations[iteration_number] = IterationInfo(
number=iteration_number,
path=iter_dir,
has_models=True,
)
return iter_dir
def record_result(
self,
iteration_number: int,
mass: float,
displacement: float,
stress: float,
) -> None:
"""Record results for an iteration and run retention check.
Args:
iteration_number: Trial number.
mass: Extracted mass in kg.
displacement: Tip displacement in mm.
stress: Max von Mises stress in MPa.
"""
if iteration_number in self._iterations:
info = self._iterations[iteration_number]
info.mass = mass
info.displacement = displacement
info.stress = stress
info.feasible = displacement <= 10.0 and stress <= 130.0
# Apply retention every 5 iterations to keep disk in check
if iteration_number % 5 == 0:
self.apply_retention()
def apply_retention(self) -> None:
"""Apply the smart retention policy.
Keep full model files for:
1. Last `keep_recent` iterations (rolling window)
2. Best `keep_best` iterations (by mass, feasible first)
Strip model files from everything else (keep solver outputs only).
"""
if not self._iterations:
return
all_nums = sorted(self._iterations.keys())
# Set 1: Last N iterations
recent_set = set(all_nums[-self.keep_recent:])
# Set 2: Best K by objective (feasible first, then lowest mass)
sorted_by_quality = sorted(
self._iterations.values(),
key=lambda info: (
0 if info.feasible else 1, # feasible first
info.mass, # then lowest mass
),
)
best_set = {info.number for info in sorted_by_quality[:self.keep_best]}
# Keep set = recent best
keep_set = recent_set | best_set
# Strip model files from everything NOT in keep set
stripped = 0
for num, info in self._iterations.items():
if num not in keep_set and info.has_models:
self._strip_models(info)
stripped += 1
if stripped > 0:
logger.info(
"Retention: kept %d recent + %d best, stripped %d iterations",
len(recent_set), len(best_set), stripped,
)
def _strip_models(self, info: IterationInfo) -> None:
"""Remove model files from an iteration folder, keep solver outputs."""
if not info.path.exists():
return
removed = 0
for f in info.path.iterdir():
if f.is_file() and f.suffix in MODEL_EXTENSIONS:
f.unlink()
removed += 1
info.has_models = False
if removed > 0:
logger.debug(
"Stripped %d model files from iter%03d",
removed, info.number,
)
def get_best_iterations(self, n: int = 3) -> list[IterationInfo]:
"""Return the N best iterations (feasible first, then lowest mass)."""
return sorted(
self._iterations.values(),
key=lambda info: (
0 if info.feasible else 1,
info.mass,
),
)[:n]
"""Smart iteration folder management for Hydrotech Beam optimization.
Manages iteration folders with intelligent retention:
- Each iteration gets a full copy of model files (openable in NX for debug)
- Last N iterations: keep full model files (rolling window)
- Best K iterations: keep full model files (by objective value)
- All others: strip model files, keep only solver outputs + params
This gives debuggability (open any recent/best iteration in NX) while
keeping disk usage bounded.
References:
CEO design brief (2026-02-11): "all models properly saved in their
iteration folder, keep last 10, keep best 3, delete stacking models"
"""
from __future__ import annotations
import json
import logging
import shutil
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
# NX model file extensions (copied to each iteration)
MODEL_EXTENSIONS = {".prt", ".fem", ".sim"}
# Solver output extensions (always kept, even after stripping)
KEEP_EXTENSIONS = {".op2", ".f06", ".dat", ".log", ".json", ".txt", ".csv"}
# Default retention policy
DEFAULT_KEEP_RECENT = 10 # keep last N iterations with full models
DEFAULT_KEEP_BEST = 3 # keep best K iterations with full models
@dataclass
class IterationInfo:
"""Metadata for a single iteration."""
number: int
path: Path
mass: float = float("inf")
displacement: float = float("inf")
stress: float = float("inf")
feasible: bool = False
has_models: bool = True # False after stripping
@dataclass
class IterationManager:
"""Manages iteration folders with smart retention.
Usage:
mgr = IterationManager(study_dir, master_model_dir)
# Before each trial:
iter_dir = mgr.prepare_iteration(iteration_number)
# After trial completes:
mgr.record_result(iteration_number, mass=..., displacement=..., stress=...)
# Periodically or at study end:
mgr.apply_retention()
"""
study_dir: Path
master_model_dir: Path
keep_recent: int = DEFAULT_KEEP_RECENT
keep_best: int = DEFAULT_KEEP_BEST
_iterations: dict[int, IterationInfo] = field(default_factory=dict, repr=False)
def __post_init__(self) -> None:
self.iterations_dir = self.study_dir / "iterations"
self.iterations_dir.mkdir(parents=True, exist_ok=True)
# Scan existing iterations (for resume support)
for d in sorted(self.iterations_dir.iterdir()):
if d.is_dir() and d.name.startswith("iter"):
try:
num = int(d.name.replace("iter", ""))
info = IterationInfo(number=num, path=d)
# Load results if available
results_file = d / "results.json"
if results_file.exists():
data = json.loads(results_file.read_text())
info.mass = data.get("mass_kg", float("inf"))
info.displacement = data.get("tip_displacement_mm", float("inf"))
info.stress = data.get("max_von_mises_mpa", float("inf"))
info.feasible = (
info.displacement <= 10.0 and info.stress <= 130.0
)
# Check if model files are present
info.has_models = any(
f.suffix in MODEL_EXTENSIONS for f in d.iterdir()
)
self._iterations[num] = info
except (ValueError, json.JSONDecodeError):
continue
if self._iterations:
logger.info(
"Loaded %d existing iterations (resume support)",
len(self._iterations),
)
def prepare_iteration(self, iteration_number: int) -> Path:
"""Set up an iteration folder with fresh model copies.
Copies all model files from master_model_dir to the iteration folder.
All paths are resolved to absolute to avoid NX reference issues.
Args:
iteration_number: Trial number (1-indexed).
Returns:
Absolute path to the iteration folder.
"""
iter_dir = (self.iterations_dir / f"iter{iteration_number:03d}").resolve()
# Clean up if exists (failed previous run)
if iter_dir.exists():
shutil.rmtree(iter_dir)
iter_dir.mkdir(parents=True)
# Copy ALL model files (so NX can resolve references within the folder)
master = self.master_model_dir.resolve()
copied = 0
for ext in MODEL_EXTENSIONS:
for src in master.glob(f"*{ext}"):
shutil.copy2(src, iter_dir / src.name)
copied += 1
logger.info(
"Prepared iter%03d: copied %d model files to %s",
iteration_number, copied, iter_dir,
)
# Track iteration
self._iterations[iteration_number] = IterationInfo(
number=iteration_number,
path=iter_dir,
has_models=True,
)
return iter_dir
def record_result(
self,
iteration_number: int,
mass: float,
displacement: float,
stress: float,
) -> None:
"""Record results for an iteration and run retention check.
Args:
iteration_number: Trial number.
mass: Extracted mass in kg.
displacement: Tip displacement in mm.
stress: Max von Mises stress in MPa.
"""
if iteration_number in self._iterations:
info = self._iterations[iteration_number]
info.mass = mass
info.displacement = displacement
info.stress = stress
info.feasible = displacement <= 10.0 and stress <= 130.0
# Apply retention every 5 iterations to keep disk in check
if iteration_number % 5 == 0:
self.apply_retention()
def apply_retention(self) -> None:
"""Apply the smart retention policy.
Keep full model files for:
1. Last `keep_recent` iterations (rolling window)
2. Best `keep_best` iterations (by mass, feasible first)
Strip model files from everything else (keep solver outputs only).
"""
if not self._iterations:
return
all_nums = sorted(self._iterations.keys())
# Set 1: Last N iterations
recent_set = set(all_nums[-self.keep_recent:])
# Set 2: Best K by objective (feasible first, then lowest mass)
sorted_by_quality = sorted(
self._iterations.values(),
key=lambda info: (
0 if info.feasible else 1, # feasible first
info.mass, # then lowest mass
),
)
best_set = {info.number for info in sorted_by_quality[:self.keep_best]}
# Keep set = recent best
keep_set = recent_set | best_set
# Strip model files from everything NOT in keep set
stripped = 0
for num, info in self._iterations.items():
if num not in keep_set and info.has_models:
self._strip_models(info)
stripped += 1
if stripped > 0:
logger.info(
"Retention: kept %d recent + %d best, stripped %d iterations",
len(recent_set), len(best_set), stripped,
)
def _strip_models(self, info: IterationInfo) -> None:
"""Remove model files from an iteration folder, keep solver outputs."""
if not info.path.exists():
return
removed = 0
for f in info.path.iterdir():
if f.is_file() and f.suffix in MODEL_EXTENSIONS:
f.unlink()
removed += 1
info.has_models = False
if removed > 0:
logger.debug(
"Stripped %d model files from iter%03d",
removed, info.number,
)
def get_best_iterations(self, n: int = 3) -> list[IterationInfo]:
"""Return the N best iterations (feasible first, then lowest mass)."""
return sorted(
self._iterations.values(),
key=lambda info: (
0 if info.feasible else 1,
info.mass,
),
)[:n]

View File

@@ -0,0 +1,249 @@
"""Smart iteration folder management for Hydrotech Beam optimization.
Manages iteration folders with intelligent retention:
- Each iteration gets a full copy of model files (openable in NX for debug)
- Last N iterations: keep full model files (rolling window)
- Best K iterations: keep full model files (by objective value)
- All others: strip model files, keep only solver outputs + params
This gives debuggability (open any recent/best iteration in NX) while
keeping disk usage bounded.
References:
CEO design brief (2026-02-11): "all models properly saved in their
iteration folder, keep last 10, keep best 3, delete stacking models"
"""
from __future__ import annotations
import json
import logging
import shutil
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
# NX model file extensions (copied to each iteration)
MODEL_EXTENSIONS = {".prt", ".fem", ".sim"}
# Solver output extensions (always kept, even after stripping)
KEEP_EXTENSIONS = {".op2", ".f06", ".dat", ".log", ".json", ".txt", ".csv"}
# Default retention policy
DEFAULT_KEEP_RECENT = 10 # keep last N iterations with full models
DEFAULT_KEEP_BEST = 3 # keep best K iterations with full models
@dataclass
class IterationInfo:
"""Metadata for a single iteration."""
number: int
path: Path
mass: float = float("inf")
displacement: float = float("inf")
stress: float = float("inf")
feasible: bool = False
has_models: bool = True # False after stripping
@dataclass
class IterationManager:
"""Manages iteration folders with smart retention.
Usage:
mgr = IterationManager(study_dir, master_model_dir)
# Before each trial:
iter_dir = mgr.prepare_iteration(iteration_number)
# After trial completes:
mgr.record_result(iteration_number, mass=..., displacement=..., stress=...)
# Periodically or at study end:
mgr.apply_retention()
"""
study_dir: Path
master_model_dir: Path
keep_recent: int = DEFAULT_KEEP_RECENT
keep_best: int = DEFAULT_KEEP_BEST
_iterations: dict[int, IterationInfo] = field(default_factory=dict, repr=False)
def __post_init__(self) -> None:
self.iterations_dir = self.study_dir / "iterations"
self.iterations_dir.mkdir(parents=True, exist_ok=True)
# Scan existing iterations (for resume support)
for d in sorted(self.iterations_dir.iterdir()):
if d.is_dir() and d.name.startswith("iter"):
try:
num = int(d.name.replace("iter", ""))
info = IterationInfo(number=num, path=d)
# Load results if available
results_file = d / "results.json"
if results_file.exists():
data = json.loads(results_file.read_text())
info.mass = data.get("mass_kg", float("inf"))
info.displacement = data.get("tip_displacement_mm", float("inf"))
info.stress = data.get("max_von_mises_mpa", float("inf"))
info.feasible = (
info.displacement <= 10.0 and info.stress <= 130.0
)
# Check if model files are present
info.has_models = any(
f.suffix in MODEL_EXTENSIONS for f in d.iterdir()
)
self._iterations[num] = info
except (ValueError, json.JSONDecodeError):
continue
if self._iterations:
logger.info(
"Loaded %d existing iterations (resume support)",
len(self._iterations),
)
def prepare_iteration(self, iteration_number: int) -> Path:
"""Set up an iteration folder with fresh model copies.
Copies all model files from master_model_dir to the iteration folder.
All paths are resolved to absolute to avoid NX reference issues.
Args:
iteration_number: Trial number (1-indexed).
Returns:
Absolute path to the iteration folder.
"""
iter_dir = (self.iterations_dir / f"iter{iteration_number:03d}").resolve()
# Clean up if exists (failed previous run)
if iter_dir.exists():
shutil.rmtree(iter_dir)
iter_dir.mkdir(parents=True)
# Copy ALL model files (so NX can resolve references within the folder)
master = self.master_model_dir.resolve()
copied = 0
for ext in MODEL_EXTENSIONS:
for src in master.glob(f"*{ext}"):
shutil.copy2(src, iter_dir / src.name)
copied += 1
logger.info(
"Prepared iter%03d: copied %d model files to %s",
iteration_number, copied, iter_dir,
)
# Track iteration
self._iterations[iteration_number] = IterationInfo(
number=iteration_number,
path=iter_dir,
has_models=True,
)
return iter_dir
def record_result(
self,
iteration_number: int,
mass: float,
displacement: float,
stress: float,
) -> None:
"""Record results for an iteration and run retention check.
Args:
iteration_number: Trial number.
mass: Extracted mass in kg.
displacement: Tip displacement in mm.
stress: Max von Mises stress in MPa.
"""
if iteration_number in self._iterations:
info = self._iterations[iteration_number]
info.mass = mass
info.displacement = displacement
info.stress = stress
info.feasible = displacement <= 10.0 and stress <= 130.0
# Apply retention every 5 iterations to keep disk in check
if iteration_number % 5 == 0:
self.apply_retention()
def apply_retention(self) -> None:
"""Apply the smart retention policy.
Keep full model files for:
1. Last `keep_recent` iterations (rolling window)
2. Best `keep_best` iterations (by mass, feasible first)
Strip model files from everything else (keep solver outputs only).
"""
if not self._iterations:
return
all_nums = sorted(self._iterations.keys())
# Set 1: Last N iterations
recent_set = set(all_nums[-self.keep_recent:])
# Set 2: Best K by objective (feasible first, then lowest mass)
sorted_by_quality = sorted(
self._iterations.values(),
key=lambda info: (
0 if info.feasible else 1, # feasible first
info.mass, # then lowest mass
),
)
best_set = {info.number for info in sorted_by_quality[:self.keep_best]}
# Keep set = recent best
keep_set = recent_set | best_set
# Strip model files from everything NOT in keep set
stripped = 0
for num, info in self._iterations.items():
if num not in keep_set and info.has_models:
self._strip_models(info)
stripped += 1
if stripped > 0:
logger.info(
"Retention: kept %d recent + %d best, stripped %d iterations",
len(recent_set), len(best_set), stripped,
)
def _strip_models(self, info: IterationInfo) -> None:
"""Remove model files from an iteration folder, keep solver outputs."""
if not info.path.exists():
return
removed = 0
for f in info.path.iterdir():
if f.is_file() and f.suffix in MODEL_EXTENSIONS:
f.unlink()
removed += 1
info.has_models = False
if removed > 0:
logger.debug(
"Stripped %d model files from iter%03d",
removed, info.number,
)
def get_best_iterations(self, n: int = 3) -> list[IterationInfo]:
"""Return the N best iterations (feasible first, then lowest mass)."""
return sorted(
self._iterations.values(),
key=lambda info: (
0 if info.feasible else 1,
info.mass,
),
)[:n]

View File

@@ -1 +1 @@
p173=1133.0042670507723
1133.0042670507721

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 1133.0042670507721,
"mass_g": 1133004.2670507722,
"volume_mm3": 143928387.58266923,
"surface_area_mm2": 9629135.962798359,
"center_of_gravity_mm": [
2500.0,
-3.9849188813805173e-13,
-9.732850072582063e-13
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -1,8 +1,8 @@
{
"iteration": 1,
"mass_kg": NaN,
"tip_displacement_mm": 19.556875228881836,
"max_von_mises_mpa": 117.484125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 1,
"mass_kg": 1133.0042670507721,
"tip_displacement_mm": 19.556875228881836,
"max_von_mises_mpa": 117.484125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 1,
"mass_kg": NaN,
"tip_displacement_mm": 19.556875228881836,
"max_von_mises_mpa": 117.484125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -1 +1 @@
p173=1266.203458914867
1266.2034589148668

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 1266.2034589148668,
"mass_g": 1266203.458914867,
"volume_mm3": 160849016.63044548,
"surface_area_mm2": 10302410.46378126,
"center_of_gravity_mm": [
2499.9999999999995,
-4.5228728964456235e-13,
-1.0297952705302583e-12
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -0,0 +1,4 @@
beam_half_core_thickness=16.7813185433211 [MilliMeter]
beam_face_thickness=26.83364680743843 [MilliMeter]
holes_diameter=192.42062402658527 [MilliMeter]
hole_count=8.0 [Constant]

View File

@@ -1,8 +1,8 @@
{
"iteration": 2,
"mass_kg": NaN,
"tip_displacement_mm": 24.064523696899414,
"max_von_mises_mpa": 398.4295,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 2,
"mass_kg": 1266.2034589148668,
"tip_displacement_mm": 24.064523696899414,
"max_von_mises_mpa": 398.4295,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 2,
"mass_kg": NaN,
"tip_displacement_mm": 24.064523696899414,
"max_von_mises_mpa": 398.4295,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -1 +1 @@
p173=1109.9630535376045
1109.963053537604

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 1109.963053537604,
"mass_g": 1109963.0535376042,
"volume_mm3": 141001404.15874043,
"surface_area_mm2": 10224850.427177388,
"center_of_gravity_mm": [
2500.0,
-4.038805106202274e-13,
-8.979130674139967e-13
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -1,15 +1,15 @@
{
"iteration": 3,
"expressions": {
"beam_half_core_thickness": 16.654851585575436,
"beam_face_thickness": 25.92976843726266,
"holes_diameter": 249.7752118546045,
"hole_count": 7.0
},
"trial_input": {
"beam_half_core_thickness": 16.654851585575436,
"beam_face_thickness": 25.92976843726266,
"holes_diameter": 249.7752118546045,
"hole_count": 7
}
{
"iteration": 3,
"expressions": {
"beam_half_core_thickness": 16.654851585575436,
"beam_face_thickness": 25.92976843726266,
"holes_diameter": 249.7752118546045,
"hole_count": 7.0
},
"trial_input": {
"beam_half_core_thickness": 16.654851585575436,
"beam_face_thickness": 25.92976843726266,
"holes_diameter": 249.7752118546045,
"hole_count": 7
}
}

View File

@@ -0,0 +1,15 @@
{
"iteration": 3,
"expressions": {
"beam_half_core_thickness": 16.654851585575436,
"beam_face_thickness": 25.92976843726266,
"holes_diameter": 249.7752118546045,
"hole_count": 7.0
},
"trial_input": {
"beam_half_core_thickness": 16.654851585575436,
"beam_face_thickness": 25.92976843726266,
"holes_diameter": 249.7752118546045,
"hole_count": 7
}
}

View File

@@ -1,8 +1,8 @@
{
"iteration": 3,
"mass_kg": NaN,
"tip_displacement_mm": 18.68077850341797,
"max_von_mises_mpa": 114.6584609375,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 3,
"mass_kg": 1109.963053537604,
"tip_displacement_mm": 18.68077850341797,
"max_von_mises_mpa": 114.6584609375,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 3,
"mass_kg": NaN,
"tip_displacement_mm": 18.68077850341797,
"max_von_mises_mpa": 114.6584609375,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -1 +1 @@
p173=1718.1024059936985
1718.102405993698

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 1718.102405993698,
"mass_g": 1718102.4059936982,
"volume_mm3": 218254878.8101751,
"surface_area_mm2": 9526891.624035092,
"center_of_gravity_mm": [
2499.9999999999995,
-2.513603829927714e-13,
-5.326334731363442e-13
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -1,15 +1,15 @@
{
"iteration": 4,
"expressions": {
"beam_half_core_thickness": 39.4879581560391,
"beam_face_thickness": 37.70634303203751,
"holes_diameter": 317.49333407316067,
"hole_count": 10.0
},
"trial_input": {
"beam_half_core_thickness": 39.4879581560391,
"beam_face_thickness": 37.70634303203751,
"holes_diameter": 317.49333407316067,
"hole_count": 10
}
{
"iteration": 4,
"expressions": {
"beam_half_core_thickness": 39.4879581560391,
"beam_face_thickness": 37.70634303203751,
"holes_diameter": 317.49333407316067,
"hole_count": 10.0
},
"trial_input": {
"beam_half_core_thickness": 39.4879581560391,
"beam_face_thickness": 37.70634303203751,
"holes_diameter": 317.49333407316067,
"hole_count": 10
}
}

View File

@@ -0,0 +1,15 @@
{
"iteration": 4,
"expressions": {
"beam_half_core_thickness": 39.4879581560391,
"beam_face_thickness": 37.70634303203751,
"holes_diameter": 317.49333407316067,
"hole_count": 10.0
},
"trial_input": {
"beam_half_core_thickness": 39.4879581560391,
"beam_face_thickness": 37.70634303203751,
"holes_diameter": 317.49333407316067,
"hole_count": 10
}
}

View File

@@ -1,8 +1,8 @@
{
"iteration": 4,
"mass_kg": NaN,
"tip_displacement_mm": 12.852874755859375,
"max_von_mises_mpa": 81.574234375,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 4,
"mass_kg": 1718.102405993698,
"tip_displacement_mm": 12.852874755859375,
"max_von_mises_mpa": 81.574234375,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 4,
"mass_kg": NaN,
"tip_displacement_mm": 12.852874755859375,
"max_von_mises_mpa": 81.574234375,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -1 +1 @@
p173=1205.9185440163983
1205.918544016398

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 1205.918544016398,
"mass_g": 1205918.544016398,
"volume_mm3": 153190871.95330262,
"surface_area_mm2": 10217809.311285218,
"center_of_gravity_mm": [
2532.261923730059,
-3.772285845536082e-13,
-8.015628081698969e-13
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -1,15 +1,15 @@
{
"iteration": 5,
"expressions": {
"beam_half_core_thickness": 18.667249127790498,
"beam_face_thickness": 27.961709646337493,
"holes_diameter": 215.2919645872769,
"hole_count": 12.0
},
"trial_input": {
"beam_half_core_thickness": 18.667249127790498,
"beam_face_thickness": 27.961709646337493,
"holes_diameter": 215.2919645872769,
"hole_count": 12
}
{
"iteration": 5,
"expressions": {
"beam_half_core_thickness": 18.667249127790498,
"beam_face_thickness": 27.961709646337493,
"holes_diameter": 215.2919645872769,
"hole_count": 12.0
},
"trial_input": {
"beam_half_core_thickness": 18.667249127790498,
"beam_face_thickness": 27.961709646337493,
"holes_diameter": 215.2919645872769,
"hole_count": 12
}
}

View File

@@ -0,0 +1,15 @@
{
"iteration": 5,
"expressions": {
"beam_half_core_thickness": 18.667249127790498,
"beam_face_thickness": 27.961709646337493,
"holes_diameter": 215.2919645872769,
"hole_count": 12.0
},
"trial_input": {
"beam_half_core_thickness": 18.667249127790498,
"beam_face_thickness": 27.961709646337493,
"holes_diameter": 215.2919645872769,
"hole_count": 12
}
}

View File

@@ -1,8 +1,8 @@
{
"iteration": 5,
"mass_kg": NaN,
"tip_displacement_mm": 17.29557228088379,
"max_von_mises_mpa": 106.283703125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 5,
"mass_kg": 1205.918544016398,
"tip_displacement_mm": 17.29557228088379,
"max_von_mises_mpa": 106.283703125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 5,
"mass_kg": NaN,
"tip_displacement_mm": 17.29557228088379,
"max_von_mises_mpa": 106.283703125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -1 +1 @@
p173=1085.4467002717115
1085.4467002717115

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 1085.4467002717115,
"mass_g": 1085446.7002717115,
"volume_mm3": 137887030.0141911,
"surface_area_mm2": 10367090.942113385,
"center_of_gravity_mm": [
2509.0298234434026,
-3.739827148611049e-13,
-8.302431198574242e-13
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -1,15 +1,15 @@
{
"iteration": 6,
"expressions": {
"beam_half_core_thickness": 10.145147355948776,
"beam_face_thickness": 33.3870327520718,
"holes_diameter": 197.6501835498656,
"hole_count": 11.0
},
"trial_input": {
"beam_half_core_thickness": 10.145147355948776,
"beam_face_thickness": 33.3870327520718,
"holes_diameter": 197.6501835498656,
"hole_count": 11
}
{
"iteration": 6,
"expressions": {
"beam_half_core_thickness": 10.145147355948776,
"beam_face_thickness": 33.3870327520718,
"holes_diameter": 197.6501835498656,
"hole_count": 11.0
},
"trial_input": {
"beam_half_core_thickness": 10.145147355948776,
"beam_face_thickness": 33.3870327520718,
"holes_diameter": 197.6501835498656,
"hole_count": 11
}
}

View File

@@ -0,0 +1,15 @@
{
"iteration": 6,
"expressions": {
"beam_half_core_thickness": 10.145147355948776,
"beam_face_thickness": 33.3870327520718,
"holes_diameter": 197.6501835498656,
"hole_count": 11.0
},
"trial_input": {
"beam_half_core_thickness": 10.145147355948776,
"beam_face_thickness": 33.3870327520718,
"holes_diameter": 197.6501835498656,
"hole_count": 11
}
}

View File

@@ -1,8 +1,8 @@
{
"iteration": 6,
"mass_kg": NaN,
"tip_displacement_mm": 17.468721389770508,
"max_von_mises_mpa": 108.2625078125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 6,
"mass_kg": 1085.4467002717115,
"tip_displacement_mm": 17.468721389770508,
"max_von_mises_mpa": 108.2625078125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 6,
"mass_kg": NaN,
"tip_displacement_mm": 17.468721389770508,
"max_von_mises_mpa": 108.2625078125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -1 +1 @@
p173=1722.6740568945102
1722.674056894509

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 1722.674056894509,
"mass_g": 1722674.0568945091,
"volume_mm3": 218835627.1461522,
"surface_area_mm2": 10233741.689197954,
"center_of_gravity_mm": [
2499.9999999999995,
-3.37147834654746e-13,
-6.319681629445577e-13
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -1,15 +1,15 @@
{
"iteration": 7,
"expressions": {
"beam_half_core_thickness": 36.424864472055134,
"beam_face_thickness": 22.317342276314122,
"holes_diameter": 221.080294100253,
"hole_count": 5.0
},
"trial_input": {
"beam_half_core_thickness": 36.424864472055134,
"beam_face_thickness": 22.317342276314122,
"holes_diameter": 221.080294100253,
"hole_count": 5
}
{
"iteration": 7,
"expressions": {
"beam_half_core_thickness": 36.424864472055134,
"beam_face_thickness": 22.317342276314122,
"holes_diameter": 221.080294100253,
"hole_count": 5.0
},
"trial_input": {
"beam_half_core_thickness": 36.424864472055134,
"beam_face_thickness": 22.317342276314122,
"holes_diameter": 221.080294100253,
"hole_count": 5
}
}

View File

@@ -0,0 +1,15 @@
{
"iteration": 7,
"expressions": {
"beam_half_core_thickness": 36.424864472055134,
"beam_face_thickness": 22.317342276314122,
"holes_diameter": 221.080294100253,
"hole_count": 5.0
},
"trial_input": {
"beam_half_core_thickness": 36.424864472055134,
"beam_face_thickness": 22.317342276314122,
"holes_diameter": 221.080294100253,
"hole_count": 5
}
}

View File

@@ -1,8 +1,8 @@
{
"iteration": 7,
"mass_kg": NaN,
"tip_displacement_mm": 15.069981575012207,
"max_von_mises_mpa": 94.8715,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 7,
"mass_kg": 1722.674056894509,
"tip_displacement_mm": 15.069981575012207,
"max_von_mises_mpa": 94.8715,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 7,
"mass_kg": NaN,
"tip_displacement_mm": 15.069981575012207,
"max_von_mises_mpa": 94.8715,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -1 +1 @@
p173=1182.0927892029772
1182.0927892029772

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 1182.0927892029772,
"mass_g": 1182092.7892029772,
"volume_mm3": 150164226.27070343,
"surface_area_mm2": 9735417.428499741,
"center_of_gravity_mm": [
2499.9999999999995,
-3.655180487736917e-13,
-8.402491975466155e-13
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -1,15 +1,15 @@
{
"iteration": 8,
"expressions": {
"beam_half_core_thickness": 21.20971666430637,
"beam_face_thickness": 27.18728441912208,
"holes_diameter": 333.33951480703604,
"hole_count": 7.0
},
"trial_input": {
"beam_half_core_thickness": 21.20971666430637,
"beam_face_thickness": 27.18728441912208,
"holes_diameter": 333.33951480703604,
"hole_count": 7
}
{
"iteration": 8,
"expressions": {
"beam_half_core_thickness": 21.20971666430637,
"beam_face_thickness": 27.18728441912208,
"holes_diameter": 333.33951480703604,
"hole_count": 7.0
},
"trial_input": {
"beam_half_core_thickness": 21.20971666430637,
"beam_face_thickness": 27.18728441912208,
"holes_diameter": 333.33951480703604,
"hole_count": 7
}
}

View File

@@ -0,0 +1,15 @@
{
"iteration": 8,
"expressions": {
"beam_half_core_thickness": 21.20971666430637,
"beam_face_thickness": 27.18728441912208,
"holes_diameter": 333.33951480703604,
"hole_count": 7.0
},
"trial_input": {
"beam_half_core_thickness": 21.20971666430637,
"beam_face_thickness": 27.18728441912208,
"holes_diameter": 333.33951480703604,
"hole_count": 7
}
}

View File

@@ -1,8 +1,8 @@
{
"iteration": 8,
"mass_kg": NaN,
"tip_displacement_mm": 18.190187454223633,
"max_von_mises_mpa": 123.9728203125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 8,
"mass_kg": 1182.0927892029772,
"tip_displacement_mm": 18.190187454223633,
"max_von_mises_mpa": 123.9728203125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 8,
"mass_kg": NaN,
"tip_displacement_mm": 18.190187454223633,
"max_von_mises_mpa": 123.9728203125,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -1 +1 @@
p173=1185.767642455542
1185.7676424555418

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 1185.7676424555418,
"mass_g": 1185767.6424555418,
"volume_mm3": 150631052.141202,
"surface_area_mm2": 10301945.288366752,
"center_of_gravity_mm": [
2499.9999999999995,
-4.4050129115418514e-13,
-9.788917581204114e-13
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -1,15 +1,15 @@
{
"iteration": 9,
"expressions": {
"beam_half_core_thickness": 25.10064411916288,
"beam_face_thickness": 15.259636308480797,
"holes_diameter": 202.9393633023646,
"hole_count": 8.0
},
"trial_input": {
"beam_half_core_thickness": 25.10064411916288,
"beam_face_thickness": 15.259636308480797,
"holes_diameter": 202.9393633023646,
"hole_count": 8
}
{
"iteration": 9,
"expressions": {
"beam_half_core_thickness": 25.10064411916288,
"beam_face_thickness": 15.259636308480797,
"holes_diameter": 202.9393633023646,
"hole_count": 8.0
},
"trial_input": {
"beam_half_core_thickness": 25.10064411916288,
"beam_face_thickness": 15.259636308480797,
"holes_diameter": 202.9393633023646,
"hole_count": 8
}
}

View File

@@ -0,0 +1,15 @@
{
"iteration": 9,
"expressions": {
"beam_half_core_thickness": 25.10064411916288,
"beam_face_thickness": 15.259636308480797,
"holes_diameter": 202.9393633023646,
"hole_count": 8.0
},
"trial_input": {
"beam_half_core_thickness": 25.10064411916288,
"beam_face_thickness": 15.259636308480797,
"holes_diameter": 202.9393633023646,
"hole_count": 8
}
}

View File

@@ -1,8 +1,8 @@
{
"iteration": 9,
"mass_kg": NaN,
"tip_displacement_mm": 21.658220291137695,
"max_von_mises_mpa": 202.82671875,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 9,
"mass_kg": 1185.7676424555418,
"tip_displacement_mm": 21.658220291137695,
"max_von_mises_mpa": 202.82671875,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 9,
"mass_kg": NaN,
"tip_displacement_mm": 21.658220291137695,
"max_von_mises_mpa": 202.82671875,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -1 +1 @@
p173=987.0960407947643
987.0960407947646

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 987.0960407947646,
"mass_g": 987096.0407947645,
"volume_mm3": 125393297.86518857,
"surface_area_mm2": 9660676.047581872,
"center_of_gravity_mm": [
2593.3817905056576,
-4.790112500292729e-13,
-1.1348015633811292e-12
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -1,15 +1,15 @@
{
"iteration": 10,
"expressions": {
"beam_half_core_thickness": 23.5377088486766,
"beam_face_thickness": 15.77772417637908,
"holes_diameter": 295.1158776920038,
"hole_count": 12.0
},
"trial_input": {
"beam_half_core_thickness": 23.5377088486766,
"beam_face_thickness": 15.77772417637908,
"holes_diameter": 295.1158776920038,
"hole_count": 12
}
{
"iteration": 10,
"expressions": {
"beam_half_core_thickness": 23.5377088486766,
"beam_face_thickness": 15.77772417637908,
"holes_diameter": 295.1158776920038,
"hole_count": 12.0
},
"trial_input": {
"beam_half_core_thickness": 23.5377088486766,
"beam_face_thickness": 15.77772417637908,
"holes_diameter": 295.1158776920038,
"hole_count": 12
}
}

View File

@@ -0,0 +1,15 @@
{
"iteration": 10,
"expressions": {
"beam_half_core_thickness": 23.5377088486766,
"beam_face_thickness": 15.77772417637908,
"holes_diameter": 295.1158776920038,
"hole_count": 12.0
},
"trial_input": {
"beam_half_core_thickness": 23.5377088486766,
"beam_face_thickness": 15.77772417637908,
"holes_diameter": 295.1158776920038,
"hole_count": 12
}
}

View File

@@ -1,8 +1,8 @@
{
"iteration": 10,
"mass_kg": NaN,
"tip_displacement_mm": 24.221370697021484,
"max_von_mises_mpa": 190.267625,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 10,
"mass_kg": 987.0960407947646,
"tip_displacement_mm": 24.221370697021484,
"max_von_mises_mpa": 190.267625,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 10,
"mass_kg": NaN,
"tip_displacement_mm": 24.221370697021484,
"max_von_mises_mpa": 190.267625,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -1 +1 @@
p173=1082.1233483234582
1082.1233483234585

View File

@@ -0,0 +1,15 @@
{
"part_file": "Beam",
"mass_kg": 1082.1233483234585,
"mass_g": 1082123.3483234586,
"volume_mm3": 137464856.24027672,
"surface_area_mm2": 9262188.327352257,
"center_of_gravity_mm": [
2499.999999999999,
-3.268459079235936e-13,
-7.571637682920178e-13
],
"num_bodies": 1,
"success": true,
"error": null
}

View File

@@ -1,15 +1,15 @@
{
"iteration": 11,
"expressions": {
"beam_half_core_thickness": 11.932749457620524,
"beam_face_thickness": 35.9698658865046,
"holes_diameter": 357.1996739776378,
"hole_count": 9.0
},
"trial_input": {
"beam_half_core_thickness": 11.932749457620524,
"beam_face_thickness": 35.9698658865046,
"holes_diameter": 357.1996739776378,
"hole_count": 9
}
{
"iteration": 11,
"expressions": {
"beam_half_core_thickness": 11.932749457620524,
"beam_face_thickness": 35.9698658865046,
"holes_diameter": 357.1996739776378,
"hole_count": 9.0
},
"trial_input": {
"beam_half_core_thickness": 11.932749457620524,
"beam_face_thickness": 35.9698658865046,
"holes_diameter": 357.1996739776378,
"hole_count": 9
}
}

View File

@@ -0,0 +1,15 @@
{
"iteration": 11,
"expressions": {
"beam_half_core_thickness": 11.932749457620524,
"beam_face_thickness": 35.9698658865046,
"holes_diameter": 357.1996739776378,
"hole_count": 9.0
},
"trial_input": {
"beam_half_core_thickness": 11.932749457620524,
"beam_face_thickness": 35.9698658865046,
"holes_diameter": 357.1996739776378,
"hole_count": 9
}
}

View File

@@ -1,8 +1,8 @@
{
"iteration": 11,
"mass_kg": NaN,
"tip_displacement_mm": 19.1060733795166,
"max_von_mises_mpa": 133.927765625,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
{
"iteration": 11,
"mass_kg": 1082.1233483234585,
"tip_displacement_mm": 19.1060733795166,
"max_von_mises_mpa": 133.927765625,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

View File

@@ -0,0 +1,8 @@
{
"iteration": 11,
"mass_kg": NaN,
"tip_displacement_mm": 19.1060733795166,
"max_von_mises_mpa": 133.927765625,
"feasible": false,
"op2_file": "beam_sim1-solution_1.op2"
}

Some files were not shown because too many files have changed in this diff Show More