Files
Aislo/resources/tester/test_537_1_e03_prob_route.py
T
eomsangdonandClaude Opus 5.5 9db4b4de33 feat(E05): 보고서 「확률해석」 시트(있을 때만) — 측점 · 비탈 · 조건마다 결정론 Fs · 평균 · σ · Pf · β · P(F<기준) · 변수 토네이도 순 · 양식 s12 · 자리표 (537-1 · 요구 351)
- 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>
2026-10-26 05:04:56 +09:00

296 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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"]))