- E05_StabReport_Prob(새 파일): E03 확률 저장본(가장 최근) → s12 · 기본값 후보 그대로면 「사용자 확정 전」 · 아니면 「사용자 입력」 - Build: 입력 · 해석 지문 · 프로그램 몫 s12 · 없으면 시트 뺌 · Print 목차 제목 - 양식 stab_report.json s12(가로 · A~K) · stab_report_slots.md s12 · 양식 시험 시트 차례 · 빈 판 쪽 수 +1 - 시험 test_537_1_e03_prob_route: E05 시트 값 = 저장본 · 없으면 뺌 · 화면 계산 TS 요청 = 서버 ProbRequest Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
296 lines
12 KiB
Python
296 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""537-1 E03 확률 해석 길(`E03_StabSection_Router_Prob`) — 검증_sub4 동결 복사 · 측점 둘 · Bishop 원호만.
|
||
|
||
- 준비(setup): 단면 물성 = E02 값 · 기본값 후보 · 기준안전율 · 결정론 F
|
||
- 임계면 고정: 평균 줄 F = 저장된 결정론 F · Pf · β · 표본 수 · 같은 요청 = 저장본(다시 안 셈) · 기록 목록
|
||
- 대표 단면 아님 · 단면에 없는 물성 · 수위선 아닌데 지하수위 변수 · 변수 없음 = 422
|
||
- 매회 탐색 일꾼(프로세스 풀 · 진행 줄 · 결과 파일)
|
||
- E05 확률해석 시트(있을 때만 · 결과 = 저장본 · 기본값 그대로 = 사용자 확정 전 · 바꾸면 사용자 입력 표기)
|
||
- 화면 계산(logic/E03_StabSection_Prob.ts): 기본값 후보 · 물성 덮어쓰기 · 요청 꼴 = 서버 시험 요청
|
||
|
||
./venv/Scripts/python.exe -m pytest resources/tester/test_537_1_e03_prob_route.py -q
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
from uuid import UUID
|
||
|
||
import pytest
|
||
from fastapi import HTTPException
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(ROOT))
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
|
||
from common_util import common_util_project_copy as cp # noqa: E402
|
||
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_Prob as rp # 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.E03_StabSection_Engine_Station import analyze_station # noqa: E402
|
||
|
||
SOURCE_ID = "c725d747-50ae-46b0-87ce-4d4e56a53cf3" # 검증_sub4(읽기만)
|
||
PID = "d" * 8 + "-" + "d" * 4 + "-" + "d" * 4 + "-" + "d" * 4 + "-" + "d" * 12
|
||
|
||
|
||
async def _copy(target: Path) -> None:
|
||
import aiomysql
|
||
|
||
from config.config_db import DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USER
|
||
|
||
connection = await aiomysql.connect(
|
||
host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD, db=DB_NAME, autocommit=True
|
||
)
|
||
try:
|
||
await cp.copy_project(
|
||
connection, SOURCE_ID, scope="stability", new_id=PID, project_root=target
|
||
)
|
||
finally:
|
||
connection.close()
|
||
|
||
|
||
def _fast(cross, design, **kw):
|
||
return analyze_station(cross, design, **{**kw, "methods": ("bishop",), "noncircular": False})
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def project():
|
||
folder = Path(tempfile.mkdtemp(prefix="aislo_e03_prob_"))
|
||
try:
|
||
asyncio.run(_copy(folder / "copy"))
|
||
except Exception as error: # noqa: BLE001
|
||
shutil.rmtree(folder, ignore_errors=True)
|
||
pytest.skip(f"검증 원본 복사 실패: {error}")
|
||
root = folder / "copy"
|
||
mp = pytest.MonkeyPatch()
|
||
original = src.stations
|
||
mp.setattr(router.src, "stations", lambda r: original(r)[20:22])
|
||
|
||
async def fake_project(_pid):
|
||
return root, {"company_id": 1, "project_kind": "STABILITY"}
|
||
|
||
mp.setattr(e02, "_project", fake_project)
|
||
mp.setattr(worker, "analyze_station", _fast)
|
||
mp.setattr(wf, "analyze_station", _fast)
|
||
mp.setattr(router, "WORKERS", 1)
|
||
mp.setattr(rp, "WORKERS", 1)
|
||
router._JOBS.clear()
|
||
|
||
async def go():
|
||
await router.analyze2d(UUID(PID))
|
||
while router._TASKS:
|
||
await asyncio.gather(*list(router._TASKS))
|
||
|
||
asyncio.run(go())
|
||
yield root
|
||
mp.undo()
|
||
shutil.rmtree(folder, ignore_errors=True)
|
||
|
||
|
||
def _slot(root: Path) -> tuple[str, str]:
|
||
"""대표 단면 중 깎기 · 쌓기 비탈 하나."""
|
||
rows = router._latest_summary(root, "")[0]["stations"]
|
||
view.mark_representative(rows, router._overrides(root))
|
||
row = next(r for r in rows if (r.get("representative") or {}).get("on"))
|
||
return row["key"], row["sides"][0]["side"]
|
||
|
||
|
||
def _req(side, mats=("weathered_soil",), **kw):
|
||
"""단면 물성마다 c(대수정규 0.3) · φ(0.1) · γ(0.05)."""
|
||
variables = [
|
||
v
|
||
for m in mats
|
||
for v in (
|
||
{"material": m, "prop": "c", "dist": "lognormal", "cov": 0.3},
|
||
{"material": m, "prop": "phi", "cov": 0.1},
|
||
{"material": m, "prop": "gamma", "cov": 0.05},
|
||
)
|
||
]
|
||
return rp.ProbRequest(
|
||
**{"side": side, "condition": "dry", "n": 400, "variables": variables, **kw}
|
||
)
|
||
|
||
|
||
def test_setup_and_fixed_run(project, monkeypatch):
|
||
key, side = _slot(project)
|
||
s = rp.setup(project, key, side, "dry")
|
||
ids = [m["id"] for m in s["materials"]]
|
||
assert "weathered_soil" in ids and "wall" not in ids
|
||
assert s["defaults"]["properties"]["c"]["cov"] == 0.3 and s["required"] in (1.5, 1.3)
|
||
fs = s["fs_deterministic"]["bishop"]
|
||
seen = []
|
||
req = _req(side, ids)
|
||
rec = rp.run_prob(project, key, req, lambda p, m: seen.append(p))
|
||
res = rec["result"]
|
||
assert res["sensitivity"]["F_mean"] == pytest.approx(fs, rel=1e-9)
|
||
assert rec["fs_deterministic"] == fs and res["stats"]["n"] == 400
|
||
st = res["stats"]
|
||
assert 0 <= st["pf"] <= 1 and st["std"] > 0 and abs(st["mean"] - fs) < 0.1 * fs
|
||
assert st["beta_normal"] == pytest.approx((st["mean"] - 1) / st["std"])
|
||
assert seen and max(seen) <= 95
|
||
# 같은 요청 = 저장본 · 기록 목록 · setup 의 마지막 결과
|
||
calls = []
|
||
monkeypatch.setattr(rp.prob, "run", lambda **kw: calls.append(1))
|
||
again = rp.run_prob(project, key, req, lambda p, m: None)
|
||
assert not calls and again["computed_at"] == rec["computed_at"]
|
||
listed = rp.records(project)
|
||
assert [(r["station_key"], r["side"], r["condition"]) for r in listed] == [(key, side, "dry")]
|
||
assert rp.setup(project, key, side, "dry")["last"]["computed_at"] == rec["computed_at"]
|
||
|
||
|
||
def test_rejects(project):
|
||
key, side = _slot(project)
|
||
with pytest.raises(HTTPException) as e:
|
||
rp.run_prob(
|
||
project, key, _req(side, variables=[{"material": "xx", "prop": "c", "cov": 0.2}]), print
|
||
)
|
||
assert e.value.status_code == 422
|
||
with pytest.raises(HTTPException) as e:
|
||
rp.run_prob(project, key, _req(side, variables=[{"prop": "water", "std": 0.5}]), print)
|
||
assert e.value.status_code == 422 and "수위선" in e.value.detail
|
||
with pytest.raises(HTTPException) as e:
|
||
rp.run_prob(
|
||
project,
|
||
key,
|
||
_req(side, variables=[{"material": "weathered_soil", "prop": "c", "cov": 0}]),
|
||
print,
|
||
)
|
||
assert "변수가 없습니다" in e.value.detail
|
||
# 대표 단면 끔 → 422
|
||
path = project / router.REPRESENTATIVE
|
||
old = path.read_text(encoding="utf-8") if path.is_file() else None
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text(json.dumps({"overrides": {key: False}}), encoding="utf-8")
|
||
try:
|
||
with pytest.raises(HTTPException) as e:
|
||
rp.run_prob(project, key, _req(side), print)
|
||
assert "대표 단면" in e.value.detail
|
||
finally:
|
||
path.unlink() if old is None else path.write_text(old, encoding="utf-8")
|
||
|
||
|
||
def test_search_worker_process():
|
||
"""매회 탐색 일꾼 — 프로세스 풀 둘 · 진행 줄 · 결과 파일(Slide2 Tutorial 08 모형 · 작은 변동)."""
|
||
from test_537_1_e03_prob import T08, _surface, var
|
||
|
||
job = {
|
||
"model": T08,
|
||
"surface": _surface(T08),
|
||
"direction": -1,
|
||
"limits": None,
|
||
"variables": [var("c", "c", 5, 0.01), var("phi", "phi", 30, 0.01)],
|
||
"n": 4,
|
||
"mode": "search",
|
||
"workers": 2,
|
||
}
|
||
done = []
|
||
out = rp._run_search(job, done.append)
|
||
assert sorted(done) == [1, 2, 3, 4] and out["n"] == 4
|
||
assert all(abs(f - 1.139) < 0.02 for f in out["F"])
|
||
|
||
|
||
def test_e05_prob_sheet(project, monkeypatch):
|
||
"""E05 s12 — 측점 줄 = 저장본 값 · 변수 줄 = 토네이도 순 · 기본값 그대로 표기 · 없으면 시트 뺌."""
|
||
from E05_StabReport import E05_StabReport_Build as build
|
||
|
||
recs = rp.records(project)
|
||
assert len(recs) == 1
|
||
rec = recs[0]
|
||
book = build.form()
|
||
build.build_program(book, build.gather(project))
|
||
s12 = next(s for s in book["시트"] if s["id"] == "s12")
|
||
cells = s12["칸"]
|
||
st = rec["result"]["stats"]
|
||
assert cells["E6"]["값"] == pytest.approx(rec["fs_deterministic"], abs=1e-6)
|
||
assert cells["F6"]["값"] == pytest.approx(st["mean"], abs=1e-6)
|
||
assert cells["H6"]["값"] == pytest.approx(100 * st["pf"], abs=1e-6)
|
||
assert cells["J6"]["값"] == pytest.approx(st["beta_lognormal"], abs=1e-6)
|
||
assert "사용자 입력" in cells["A3"]["값"] # 표본 400 ≠ 기본 1000
|
||
d = rp.load_defaults()
|
||
plain = {
|
||
**rec["request"],
|
||
**{k: d["run"][k] for k in ("n", "sampling", "seed", "mode", "method")},
|
||
}
|
||
assert build.prob._is_default({"request": plain}, d)
|
||
plain["variables"] = [{**plain["variables"][0], "cov": 0.25}]
|
||
assert not build.prob._is_default({"request": plain}, d)
|
||
first = rec["result"]["sensitivity"]["tornado"][0]["name"]
|
||
assert cells["B10"]["값"] == first and cells["K10"]["값"] == 1
|
||
monkeypatch.setattr(build.prob, "read", lambda root: [])
|
||
empty = build.form()
|
||
build.build_program(empty, build.gather(project))
|
||
assert "s12" not in {s["id"] for s in empty["시트"]}
|
||
|
||
|
||
TYPESCRIPT = ROOT / "config" / "node_modules" / "typescript"
|
||
LOGIC_TS = ROOT / "E03_StabSection" / "logic" / "E03_StabSection_Prob.ts"
|
||
LOGIC_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 setup = JSON.parse(fs.readFileSync(0, "utf8"));
|
||
const cache = m.fillCache(setup, null);
|
||
const req = m.probRequest(setup, cache);
|
||
cache.vars[m.varKey("colluvium", "c")].value = 0.5;
|
||
process.stdout.write(JSON.stringify({ req, changed: m.changedCount(setup, cache),
|
||
ticks: m.axisTicks(0.83, 1.47, 5) }));
|
||
"""
|
||
|
||
|
||
@pytest.mark.skipif(not TYPESCRIPT.is_dir(), reason="config/node_modules 없음")
|
||
def test_logic_ts_request_matches_server():
|
||
"""기본값 후보(붕적토 c 0.4 덮어씀) → 요청 꼴 = 서버 ProbRequest 로 읽힘 · 바꾼 줄 수 · 눈금."""
|
||
setup = {
|
||
"side": "left",
|
||
"condition": "wet",
|
||
"materials": [
|
||
{"id": "weathered_soil", "name": "풍화토", "c": 15, "phi": 30, "gamma_t": 18},
|
||
{"id": "colluvium", "name": "붕적토", "c": 5, "phi": 27, "gamma_t": 17.5},
|
||
],
|
||
"groundwater": {"mode": "line", "ru": None},
|
||
"defaults": rp.load_defaults(),
|
||
}
|
||
out = subprocess.run(
|
||
["node", "-e", LOGIC_SCRIPT, str(TYPESCRIPT), str(LOGIC_TS)],
|
||
input=json.dumps(setup),
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
check=True,
|
||
)
|
||
got = json.loads(out.stdout)
|
||
req = rp.ProbRequest(**got["req"])
|
||
assert (req.side, req.condition, req.n, req.sampling, req.mode) == (
|
||
"left",
|
||
"wet",
|
||
1000,
|
||
"lhs",
|
||
"fixed",
|
||
)
|
||
by = {(v.material, v.prop): v for v in req.variables}
|
||
assert len(by) == 6 # 물 σ 0 = 뺌
|
||
assert by[("colluvium", "c")].cov == 0.4 and by[("weathered_soil", "c")].cov == 0.3
|
||
assert by[("weathered_soil", "c")].dist == "lognormal" and by[("colluvium", "phi")].cov == 0.1
|
||
assert all(v.trunc_sigma == 3.0 for v in req.variables)
|
||
assert got["changed"] == 1
|
||
assert got["ticks"][0] <= 0.83 and got["ticks"][-1] >= 1.47 and 0 not in got["ticks"]
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(pytest.main([__file__, "-q"]))
|