# -*- 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"]))