auto: daily sync
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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) — 40–50 trials
|
||||
- **Phase 2:** TPE via Optuna — 60–100 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 | 10–40 mm | 4× range | Reasonable. Lower bound = thin core, upper = thick. Stiffness-mass trade-off |
|
||||
| DV2: face_thickness | 10–40 mm | 4× range | Reasonable. 10 mm face is already substantial for steel |
|
||||
| DV3: holes_diameter | 150–450 mm | 3× range | ⚠️ **Needs geometric check** — see §4.2 |
|
||||
| DV4: hole_count | 5–15 | 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** | 60–100 | Directed optimization |
|
||||
| **Validation** | 3–5 | Confirm optimum, check mesh sensitivity |
|
||||
| **Total** | **114–156** | |
|
||||
|
||||
### 5.2 Compute Time Estimate
|
||||
|
||||
| Parameter | Estimate | Notes |
|
||||
|-----------|----------|-------|
|
||||
| DOF count | 10K–100K | Steel beam, SOL 101 |
|
||||
| Single solve time | 30s–3min | Depends on mesh density |
|
||||
| Model rebuild time | 10–30s | NX parametric update + remesh |
|
||||
| Total per trial | 1–4 min | Rebuild + solve + extraction |
|
||||
| Phase 1 (51 trials) | 1–3.5 hrs | |
|
||||
| Phase 2 (60–100 trials) | 1–7 hrs | |
|
||||
| **Total compute** | **2–10 hrs** | Likely ~4–5 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 114–156 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.*
|
||||
@@ -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` | 10–40 | Continuous | 25.162 | mm |
|
||||
| DV2 | `beam_face_thickness` | 10–40 | Continuous | 21.504 | mm |
|
||||
| DV3 | `holes_diameter` | 150–450 | Continuous | 300 | mm |
|
||||
| DV4 | `hole_count` | 5–15 | 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` | 10–40 | Continuous | 25.162 | mm |
|
||||
| DV2 | `beam_face_thickness` | 10–40 | Continuous | 21.504 | mm |
|
||||
| DV3 | `holes_diameter` | 150–450 | Continuous | 300 | mm |
|
||||
| DV4 | `hole_count` | 5–15 | 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*
|
||||
|
||||
@@ -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` | 10–40 | Continuous | 25.162 | mm |
|
||||
| DV2 | `beam_face_thickness` | 10–40 | Continuous | 21.504 | mm |
|
||||
| DV3 | `holes_diameter` | 150–450 | Continuous | 300 | mm |
|
||||
| DV4 | `hole_count` | 5–15 | 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*
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
410
projects/hydrotech-beam/studies/01_doe_landscape/geometric_checks.py
Normal file → Executable file
410
projects/hydrotech-beam/studies/01_doe_landscape/geometric_checks.py
Normal file → Executable 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 ✓")
|
||||
|
||||
@@ -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 ✓")
|
||||
470
projects/hydrotech-beam/studies/01_doe_landscape/history.py
Normal file → Executable file
470
projects/hydrotech-beam/studies/01_doe_landscape/history.py
Normal file → Executable 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()
|
||||
|
||||
@@ -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()
|
||||
498
projects/hydrotech-beam/studies/01_doe_landscape/iteration_manager.py
Normal file → Executable file
498
projects/hydrotech-beam/studies/01_doe_landscape/iteration_manager.py
Normal file → Executable 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]
|
||||
|
||||
@@ -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]
|
||||
@@ -0,0 +1 @@
|
||||
p173=1133.0042670507723
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=1133.0042670507723
|
||||
1133.0042670507721
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=1266.203458914867
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=1266.203458914867
|
||||
1266.2034589148668
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=1109.9630535376045
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=1109.9630535376045
|
||||
1109.963053537604
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=1718.1024059936985
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=1718.1024059936985
|
||||
1718.102405993698
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=1205.9185440163983
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=1205.9185440163983
|
||||
1205.918544016398
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=1085.4467002717115
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=1085.4467002717115
|
||||
1085.4467002717115
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=1722.6740568945102
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=1722.6740568945102
|
||||
1722.674056894509
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=1182.0927892029772
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=1182.0927892029772
|
||||
1182.0927892029772
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=1185.767642455542
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=1185.767642455542
|
||||
1185.7676424555418
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=987.0960407947643
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=987.0960407947643
|
||||
987.0960407947646
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=1082.1233483234582
|
||||
@@ -0,0 +1 @@
|
||||
p173=1343.5034206648418
|
||||
@@ -1 +1 @@
|
||||
p173=1082.1233483234582
|
||||
1082.1233483234585
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
p173=783.4437976533501
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user