feat(hydrotech-beam): Phase 1 LHS DoE study code
Implements the optimization study code for Phase 1 (LHS DoE) of the Hydrotech Beam structural optimization. Files added: - run_doe.py: Main entry point — Optuna study with SQLite persistence, Deb's feasibility rules, CSV/JSON export, Phase 1→2 gate check - sampling.py: 50-point LHS via scipy.stats.qmc with stratified integer sampling ensuring all 11 hole_count levels (5-15) are covered - geometric_checks.py: Pre-flight feasibility filter — hole overlap (corrected formula: span/(n-1) - d ≥ 30mm) and web clearance checks - nx_interface.py: NX automation module with stub solver for development and NXOpen template for Windows/dalidou integration - requirements.txt: optuna, scipy, numpy, pandas Key design decisions: - Baseline enqueued as Trial 0 (LAC lesson) - All 4 DV expression names from binary introspection (exact spelling) - Pre-flight geometric filter saves compute and prevents NX crashes - No surrogates (LAC lesson: direct FEA via TPE beats surrogate+L-BFGS) - SQLite persistence enables resume after interruption Tested end-to-end with stub solver: 51 trials, 12 geometric rejects, 39 solved, correct CSV/JSON output. Ref: OPTIMIZATION_STRATEGY.md, auditor review 2026-02-10
This commit is contained in:
@@ -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 ✓")
|
||||
Reference in New Issue
Block a user