Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-10-26 04:23:47 +09:00
11 changed files with 997 additions and 20 deletions
@@ -114,6 +114,7 @@
| 6 행부터 가정 하나 × 조건 하나 = 한 줄 | A 측점 · B 비탈(좌 · 우 · 깎기 · 쌓기)(서식 7) · C 가정 글(1:n 전 → 후 · 소단 폭 / 높이 · 최소 완화)(5) · D 조건(7) · E 전 Fs · F 후 Fs · G 기준(8) · H 전 판정 · I 후 판정(7 → N.G 10 · O.K 11) · 없으면 「가정 없음」 |
| 가정 줄 다음 한 줄 띄고 `8` · `9` 행 꼴(제목 3 · 머리 6) | 2. 벽 외적 안정 |
| 그 아래 벽 하나 × 경우 하나 = 한 줄 | A 측점(7) · B 벽 이름 · 평상시 / 지진 시 · 토압법 · 배면 수위(5) · C 활동 · E 전도 · G 지지력 Fs(8) · D · F · H 기준(8 · KDS 11 80 05) · I 판정(7 → 10 · 11) · 없으면 「벽 검토 없음」 |
| 네일 · 앵커 가정이 있을 때만 — 벽 줄 다음 한 줄 띄고 `8` · `9` 행 꼴(제목 · 머리 글 = 프로그램) | 3. 네일 · 앵커 · 가정 하나 × 조건 하나 × 네일 하나 = 한 줄(10 행 서식) · A 측점 · B 비탈 · 조건 · C 번호 · 종류 · D 머리 x / z · E 길이 · F 각도 · G 간격 · H 동원력 kN/m · I 지배(인장 · 인발 · 머리 쪽 빠짐 · 활동면 안 만남 · 결과 없음 「-」) · 1 절 가정 줄에도 들어감(C 가정 글 = 종류 · 개수 · 길이 · 각도 · 간격 · 최소 길이) |
- 6 행 · 10 행 서식을 줄마다 복사 · 저장본 = (측점 · 종류 · 비탈)마다 가장 최근 하나.
@@ -7,6 +7,9 @@
- 길(검증_sub2 동결 복사 · 벽 측점 + 하나): 지금 기울기 그대로 = 원본 안전율(±3 %) · 최소 완화가
목표 안전율을 맞춤 · 원본 측점 · 요약 저장본 그대로 · 벽 측점 외적 안정 · 벽 쪽 비탈 가정 거절 ·
E05 보강검토 시트(있을 때만)
- 네일 · 앵커(뒷부분): 배치 → supports 꼴 · 그림 좌표(머리 · 끝 · 정착 구간 · 활동면 교점 · mode) = 해석해(TS) ·
엔진 고리 전 = 그림만(supports_pending) · 가짜 엔진(supports 인자 · 길이 비례 Fs)으로 최소 길이 찾기 ·
E05 네일 · 앵커 줄
./venv/Scripts/python.exe -m pytest resources/tester/test_536_3_e03_reinforce.py -q
"""
@@ -14,8 +17,10 @@
from __future__ import annotations
import asyncio
import json
import math
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
@@ -33,6 +38,7 @@ from E01_StabRegister import E01_StabRegister_Source as src # noqa: E402
from E02_StabGround import E02_StabGround_Router as e02 # noqa: E402
from E03_StabSection import E03_StabSection_Router as router # noqa: E402
from E03_StabSection import E03_StabSection_Router_WhatIf as wf # noqa: E402
from E03_StabSection import E03_StabSection_View as view # noqa: E402
from E03_StabSection import E03_StabSection_Worker as worker # noqa: E402
from E03_StabSection import E03_StabSection_WallCheck as wc # noqa: E402
from E03_StabSection import E03_StabSection_WhatIf as whatif # noqa: E402
@@ -326,3 +332,145 @@ def test_E05_보강검토_시트(project, monkeypatch):
empty = build.form()
build.build_program(empty, build.gather(project))
assert "s8" not in {s["id"] for s in empty["시트"]} # 없으면 시트 뺌
# ── 네일 · 앵커(뒷부분) ──────────────────────────────────────────────────────
TYPESCRIPT = ROOT / "config" / "node_modules" / "typescript"
SUPPORT_TS = ROOT / "E03_StabSection" / "logic" / "E03_StabSection_Support.ts"
SUPPORT_SCRIPT = r"""
const ts = require(process.argv[1]);
const fs = require("fs");
const src = fs.readFileSync(process.argv[2], "utf8");
const opts = { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 };
const js = ts.transpileModule(src, { compilerOptions: opts }).outputText;
const box = { exports: {} };
new Function("exports", "module", js)(box.exports, box);
const m = box.exports;
const c = JSON.parse(fs.readFileSync(0, "utf8"));
const nail = { ...m.SUPPORT_DEFAULTS.nail, rows: 9, length_m: 6, angle_deg: 15 };
const cut = m.layoutSupports(c.surface, "left", "cut", 4.5, nail);
const anchor = { ...m.SUPPORT_DEFAULTS.anchor, rows: 1 };
const fill = m.layoutSupports(c.surface, "right", "fill", 3.0, anchor);
process.stdout.write(JSON.stringify({
cut, fill,
draws: m.supportDraws(cut, -1, c.critical, [{ index: 0, mobilized_kN_per_m: 12.5, mode: "pullout" }]),
anchor: m.supportDraws(fill, -1, null, null),
none: m.layoutSupports([[1, 0], [2, 0]], "left", "cut", 3, nail),
}));
"""
# 노면 −3 ~ 3 (EL 100) · 좌 깎기 1:1 (3, 100) → (13, 110) · 우 쌓기 1:1.5 (−3, 100) → (−7.5, 97)
SURFACE = [[13, 110], [3, 100], [-3, 100], [-7.5, 97], [-20, 97]]
CRITICAL = [[3, 100], [6, 101], [12, 109]] # 머리 (4, 101) 네일과 만나는 활동면
def test_네일_배치_supports_꼴_그림_좌표():
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", "-e", SUPPORT_SCRIPT, str(TYPESCRIPT), str(SUPPORT_TS)],
input=json.dumps({"surface": SURFACE, "critical": CRITICAL}),
capture_output=True, text=True, cwd=str(ROOT),
) # fmt: skip
assert proc.returncode == 0, proc.stderr
out = json.loads(proc.stdout)
cut = out["cut"]
# 행 = 1 · 2.5 · 4.0 m(4.5 m 비탈 안만) · 머리 = 1:1 면 위
assert [(p["x"], p["z"]) for p in cut] == [(4.0, 101.0), (5.5, 102.5), (7.0, 104.0)]
keys = {"type", "side", "x", "z", "angle_deg", "length_m", "spacing_m", "tensile_kN",
"bond_kPa", "bond_length_m", "hole_dia_mm", "prestress_kN"} # fmt: skip
assert all(set(p) == keys and p["side"] == "left" and p["prestress_kN"] == 0 for p in cut)
assert cut[0]["spacing_m"] == 1.5 and cut[0]["bond_length_m"] == 0
# 쌓기 밑 = 노면 − 3 m · 첫 행 1.5 m = EL 98.5 · 1:1.5 면 x = −3 − 1.5 × 1.5
(anchor,) = out["fill"]
assert (anchor["x"], anchor["z"], anchor["side"]) == (-5.25, 98.5, "right")
assert anchor["bond_length_m"] == 6 and anchor["prestress_kN"] == 200
assert out["none"] == []
# 그림 — 깎기 좌(활동 −x) = 비탈 속(+x)으로 · 수평 아래 15° · 네일 정착 = 전 길이
d0 = out["draws"][0]
a = math.radians(15)
tail = (4 + 6 * math.cos(a), 101 - 6 * math.sin(a))
assert d0["tail"] == pytest.approx(tail, abs=1e-3)
assert d0["bond"][0] == [4.0, 101.0] and d0["bond"][1] == pytest.approx(tail, abs=1e-3)
assert (d0["mode"], d0["force"]) == ("pullout", 12.5)
# 교점 — (3,100)~(6,101) 선 z = 100 + (x−3)/3 과 네일 z = 101 − (x−4)·tan15
t15 = math.tan(a)
x = (101 + 4 * t15 - 99) / (1 / 3 + t15)
assert d0["cross"] == pytest.approx([x, 100 + (x - 3) / 3], abs=1e-4) # 끝 1 mm 반올림
assert [d["mode"] for d in out["draws"][1:]] == ["pending", "pending"]
# 앵커 — 쌓기 우(활동 −x 바깥) = 비탈 속(+x)으로 · 정착 = 끝에서 6 m · 결과 없음
(da,) = out["anchor"]
b = math.radians(30)
want = (-5.25 + 12 * math.cos(b), 98.5 - 12 * math.sin(b))
assert da["tail"] == pytest.approx(want, abs=1e-3)
assert da["bond"][0] == pytest.approx(
(-5.25 + 6 * math.cos(b), 98.5 - 6 * math.sin(b)), abs=1e-3
)
assert da["cross"] is None and da["mode"] == "pending"
def _nails(side: str, n: int = 2) -> list[wf.SupportIn]:
return [
wf.SupportIn(type="nail", x=5.0 * (1 if side == "left" else -1), z=1.0 + i, angle_deg=15,
length_m=6, spacing_m=1.5, tensile_kN=121.6, bond_kPa=80, hole_dia_mm=100)
for i in range(n)
] # fmt: skip
def _engine_with_supports(cross, design, *, supports=None, **kw):
"""가짜 엔진 — supports 인자 · 그 비탈 대표 방법 Fs × (1 + 0.05 L) · 동원력 = 10 L."""
out = _fast(cross, design, **kw)
for case in out["cases"]:
mine = [i for i, s in enumerate(supports or []) if s["side"] == case["side"]]
best = case["result"]["methods"].get(view.PRIMARY, {}).get("min")
if not mine or not best:
continue
length = supports[mine[0]]["length_m"]
best["F"] *= 1 + 0.05 * length
case["result"]["support_forces"] = [
{"index": i, "mobilized_kN_per_m": 10.0 * length, "mode": "pullout"} for i in mine
]
return out
def test_네일_엔진_전_그림만_엔진_뒤_최소_길이(project, monkeypatch):
from E05_StabReport import E05_StabReport_Build as build
key, side, before, _ = _plain_station(project)
stamps = _stamps(project)
pending = wf.run_whatif(project, key, wf.WhatIfRequest(side=side, supports=_nails(side)))
assert pending["type"] == "support" and pending["supports_pending"] is True
assert pending["support_forces"] == {} and close(pending["after"]["dry"]["fs"], before, 1e-6)
assert [s["side"] for s in pending["assume"]["supports"]] == [side, side]
monkeypatch.setattr(wf, "analyze_station", _engine_with_supports)
assert wf.engine_takes_supports()
target = round(before * 1.24, 3) # L ≥ 4.8 m → 0.5 m 단위 = 5.0 m
ask = wf.WhatIfRequest(side=side, supports=_nails(side), search=True, condition="dry",
target_fs=target) # fmt: skip
rec = wf.run_whatif(project, key, ask)
assert rec["supports_pending"] is False and rec["search"]["found"] == 5.0
assert all(s["length_m"] == 5.0 for s in rec["assume"]["supports"])
assert rec["after"]["dry"]["fs"] >= target
assert all(not ok for n, ok in rec["search"]["tried"] if n < 5.0)
forces = rec["support_forces"]["dry"]
assert [(f["index"], f["mobilized_kN_per_m"], f["mode"]) for f in forces] == [
(0, 50.0, "pullout"),
(1, 50.0, "pullout"),
]
assert _stamps(project) == stamps # 원본 측점 · 요약 저장본 그대로
recs = wf.records(project)
assert "support" in {r["type"] for r in recs}
book = build.form()
build.build_program(book, build.gather(project))
cells = next(s for s in book["시트"] if s["id"] == "s8")["칸"]
head = next(
int(a1[1:]) for a1, c in cells.items() if str(c.get("값", "")).startswith("3. 네일")
)
assert cells[f"I{head + 1}"]["값"] == "지배"
rows = [
r
for r in range(head + 2, head + 40)
if f"C{r}" in cells and "네일" in str(cells[f"C{r}"].get("값"))
]
assert len(rows) == 2 * len(rec["after"])
assert cells[f"E{rows[0]}"]["값"] == 5.0 and cells[f"I{rows[0]}"]["값"] == "인발"
assert any("네일 2개 · L 5 m" in str(c.get("값")) for a1, c in cells.items() if a1[0] == "C")