# -*- coding: utf-8 -*- """532-5 E03 이음매 — 2D 해석 길에 벽(532-4) · 지하수 갈래(E02) · 빠진 횡단 알림(532-1 덧). - 벽: 검증_sub2 복사본 벽 선 측점(351 m 기슭막이) — 길이 벽을 넘김 · 임계면이 벽을 안 지남 · 측점 보기에 벽 폴리곤 · 목록 벽 수 · 벽 있는 측점만 지문이 바뀜 - 지하수: E02 우기 갈래를 바꾸면 측점 지문이 바뀜 · 측점 보기 수위선 = 고른 갈래 - 빠진 횡단: 횡단 파일이 없으면 result2d · stab/ground 의 missing_stations 에 그 측점 - E05 이음매: E05 가 E03 정본만 읽음(사본 없음) · 측점 수 · N.G 수 · 지배 비탈이 E03 과 같음 - result2d 덧칸: 지배 비탈 = fs / 기준 가장 낮은 쪽(깎기 1.22/1.2 · 쌓기 1.25/1.3 → 쌓기 · N.G) ./venv/Scripts/python.exe -m pytest resources/tester/test_532_5_e03_seam.py -q """ from __future__ import annotations import asyncio import json import shutil import sys import tempfile from pathlib import Path from uuid import UUID import numpy as np import pytest from shapely.geometry import LineString, Polygon ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from common_util import common_util_project_copy as cp # noqa: E402 from common_util.common_util_station_key import chainage_key # noqa: E402 from E01_StabRegister import E01_StabRegister_Source as src # noqa: E402 from E02_StabGround import E02_StabGround_Model as ground_model # 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_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 = "cb15892a-5788-4a26-9316-20a15421f68b" # 검증_sub2(읽기만) PID = "e" * 8 + "-" + "e" * 4 + "-" + "e" * 4 + "-" + "e" * 4 + "-" + "e" * 12 WALL_KEY = "351.00" # 검증_sub2 기슭막이 측점 CORNER_M = 0.2 # 임계면이 벽 모서리를 스치는 길이 한도(test_532_4 와 같음) USER = {"role": "USER", "company_id": 1} 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() @pytest.fixture(scope="module") def frozen(): folder = Path(tempfile.mkdtemp(prefix="aislo_e03_seam_")) 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" if WALL_KEY not in router._walls(str(src.source_dir(root)), "seam"): shutil.rmtree(folder, ignore_errors=True) pytest.skip("검증_sub2 에 벽 측점이 없음") yield root shutil.rmtree(folder, ignore_errors=True) def _fast(cross, design, **kw): """진짜 엔진 — Bishop 원호만(빠르게).""" return analyze_station(cross, design, **{**kw, "methods": ("bishop",), "noncircular": False}) @pytest.fixture def project(frozen, monkeypatch): """벽 측점 + 벽 없는 측점 하나 · 진짜 엔진(빠르게) · 서버 안에서 차례로.""" for name in ("_derived", "E02_StabGround", "E03_StabSection", "initial_snapshot"): shutil.rmtree(frozen / name, ignore_errors=True) original = src.stations def pick(root): rows = original(root) wall = [r for r in rows if chainage_key(r["chainage_m"]) == WALL_KEY] return wall + [r for r in rows if chainage_key(r["chainage_m"]) != WALL_KEY][:1] monkeypatch.setattr(router.src, "stations", pick) async def fake_project(_pid): return frozen, {"company_id": 1, "project_kind": "STABILITY"} monkeypatch.setattr(e02, "_project", fake_project) monkeypatch.setattr(worker, "analyze_station", _fast) monkeypatch.setattr(router, "WORKERS", 1) router._JOBS.clear() return frozen def _analyze() -> dict: async def go(): first = await router.analyze2d(UUID(PID)) while router._TASKS: await asyncio.gather(*list(router._TASKS)) return first return asyncio.run(go()) def _save_ground(root: Path, ground: dict) -> None: ground_model.ground_path(root).parent.mkdir(parents=True, exist_ok=True) ground_model.ground_path(root).write_text(json.dumps(ground), encoding="utf-8") def test_벽_측점_임계면이_벽을_안_지남(project): assert _analyze()["status"] == "accepted" assert router._JOBS[PID]["error"] is None body = asyncio.run(router.result2d(UUID(PID), USER)) walls = {r["key"]: r["walls"] for r in body["stations"]} assert walls[WALL_KEY] >= 1 and sorted(walls.values())[0] == 0 sec = asyncio.run(router.section(UUID(PID), WALL_KEY)) polys = [Polygon(r["polygon"]) for r in sec["regions"] if r["material_id"] == "wall"] assert polys and any(m["id"] == "wall" for m in sec["materials"]) crossed = 0 for a in sec["analyses"]: line = LineString(a["critical"]["points"]) for poly in polys: assert line.intersection(poly).length <= CORNER_M crossed += 1 assert crossed == len(sec["analyses"]) > 0 def test_벽_있는_측점만_지문이_바뀜(project, monkeypatch): ground = router._ground(project) with_walls = {it["key"]: it["fp"] for it in router.plan_run(project, ground)["items"]} monkeypatch.setattr(router, "_walls", lambda source, fp: {}) plain = {it["key"]: it["fp"] for it in router.plan_run(project, ground)["items"]} assert with_walls[WALL_KEY] != plain[WALL_KEY] assert [with_walls[k] == plain[k] for k in plain if k != WALL_KEY] == [True] def test_지하수_갈래_바꾸면_지문_바뀜(project): ground = router._ground(project) before = router.plan_run(project, ground) ground["groundwater"]["wet_mode"] = "ru" after = router.plan_run(project, ground) assert before["fp"] != after["fp"] assert all(a["fp"] != b["fp"] for a, b in zip(before["items"], after["items"])) def test_수위선_고른_갈래(project): _analyze() key = next(r["key"] for r in asyncio.run(router.result2d(UUID(PID), USER))["stations"]) first = asyncio.run(router.section(UUID(PID), key)) kds, surface = first["water"]["wet"], first["surface"] ground = router._ground(project) ground["groundwater"].update(wet_mode="ru", ru=0.3) _save_ground(project, ground) assert asyncio.run(router.section(UUID(PID), key))["water"]["wet"] is None # r_u = 선 없음 ground["groundwater"].update(wet_mode="depth_ratio", depth_ratio=0.5) _save_ground(project, ground) deep = asyncio.run(router.section(UUID(PID), key))["water"]["wet"] assert kds and deep # 깊이 비율 선 = 공사 뒤 지표 아래 · 비탈 쪽은 확실히 아래(연직 면 x 는 뺌 — 지표 값이 둘) sx, sz = zip(*surface) steps = {x for x in sx if sx.count(x) > 1} below = [float(np.interp(x, sx, sz)) - z for x, z in deep if x not in steps] assert min(below) >= -1e-6 and max(below) > 0.1 def test_빠진_횡단_목록(project): source = src.source_dir(project) row = src._cross_rows(source)[3] path = src.resolve_cross_file(source, row) moved = path.with_suffix(".bak") path.rename(moved) try: body = asyncio.run(router.result2d(UUID(PID), USER)) ground = asyncio.run(e02.get_ground(UUID(PID), USER)) finally: moved.rename(path) assert body["missing_stations"] == [float(row["chainage_m"])] assert ground["missing_stations"] == [float(row["chainage_m"])] assert asyncio.run(router.result2d(UUID(PID), USER))["missing_stations"] == [] def test_E05_가_E03_정본으로_보고서(project): """E03 [해석] 정본(stab2d_summary · stab2d_station)으로 E05 가 돎 — 사본 안 씀 · 수가 같음.""" from E05_StabReport import E05_StabReport_Build as build _analyze() assert not (project / "E03_StabSection" / "_derived").exists() # 옛 사본 꼴 없음 body = asyncio.run(router.result2d(UUID(PID), USER)) inputs = build.gather(project) rows = inputs["result2d"]["stations"] assert len(rows) == len(body["stations"]) == 2 assert WALL_KEY in {r["station_key"] for r in rows} ng = sum(1 for r in body["stations"] if r["verdict_overall"] == "ng") assert sum(1 for r in rows if r["verdict"] == "ng") == ng e03 = {r["station_key"]: r for r in body["stations"]} for r in rows: mine = e03[r["station_key"]] assert r["side"] == mine["side"] in ("cut", "fill") assert r["fs_by_condition"] == mine["fs_by_condition"] assert r["verdict"] == mine["verdict_overall"] in ("ok", "ng") reps = inputs["representatives"] assert reps and all(rep["result"] and rep["result"]["cases"] for rep in reps) book = build.form() jobs = build.build_program(book, inputs) assert len([k for k, svg in jobs if svg.startswith("= len(reps) # 대표 단면 끔 → E05 목록에서 빠짐 · representative.json = overrides 만 key = reps[0]["station_key"] asyncio.run( router.put_representative(UUID(PID), router.RepresentativeRequest(overrides={key: False})) ) assert key not in [r["station_key"] for r in build.gather(project)["representatives"]] saved = json.loads((project / router.REPRESENTATIVE).read_text("utf-8")) assert saved == {"overrides": {key: False}} def _case(fs, need): return {"fs": fs, "required": need, "ok": fs >= need} def test_result2d_지배_비탈_기준_대비(): """깎기 1.22/1.2(O.K) · 쌓기 1.25/1.3(N.G) — fs 만 낮은 깎기가 아니라 쌓기가 지배 · N.G.""" from E05_StabReport import E05_StabReport_Build as build row = { "key": "10.00", "chainage_m": 10.0, "sides": [{"side": "left", "role": "cut"}, {"side": "right", "role": "fill"}], "cases": { "dry": {"left": _case(1.6, 1.5), "right": _case(1.7, 1.5)}, "wet": {"left": _case(1.22, 1.2), "right": _case(1.25, 1.3)}, }, "min": {"dry": 1.6, "wet": 1.22}, "verdict": {"dry": "ok", "wet": "ng"}, } out = view.governing_row(row) assert out["side"] == "fill" and out["fs_min"] == 1.25 assert out["fs_by_condition"] == {"dry": 1.7, "wet": 1.25} assert out["verdict_overall"] == "ng" and out["method"] == "bishop" e05 = build._row2d(row, "bishop") assert (e05["side"], e05["fs_by_condition"], e05["verdict"]) == ( out["side"], out["fs_by_condition"], out["verdict_overall"], ) row["verdict"] = {"dry": "ok", "wet": "ok"} assert view.governing_row(row)["verdict_overall"] == "ok" bare = {**row, "cases": {"dry": {"left": {"fs": 1.4, "required": None}}}} assert view.governing_row(bare)["side"] is None and view.governing_row(bare)["fs_min"] == 1.22 def _wet_fs(project, key: str) -> tuple[float, float]: _analyze() assert router._JOBS[PID]["error"] is None rows = asyncio.run(router.result2d(UUID(PID), USER))["stations"] fs = next(r["fs_by_condition"] for r in rows if r["key"] == key) return fs["dry"], fs["wet"] def test_지하수_갈래_넷이_안전율을_바꿈(project, monkeypatch): """일꾼이 E02 지하수 갈래를 엔진에 넘김 — 갈래마다 우기 안전율이 다름 · KDS 기본 = 전과 같음.""" _analyze() rows = asyncio.run(router.result2d(UUID(PID), USER))["stations"] key = next(r["key"] for r in rows if r["key"] != WALL_KEY) chainage = next(r["chainage_m"] for r in rows if r["key"] == key) base = router._ground(project) def run(**water) -> tuple[float, float]: ground = json.loads(json.dumps(base)) ground["groundwater"].update(water) _save_ground(project, ground) return _wet_fs(project, key) # 전 = 일꾼이 모형 기본(우기 지표 포화 · 건기 없음)을 그대로 씀 with monkeypatch.context() as patch: patch.setattr(worker, "station_groundwater", lambda _g, _c, model: model["groundwater"]) legacy = run(wet_mode="kds") kds = run(wet_mode="kds") half = run(wet_mode="depth_ratio", depth_ratio=0.5) ru = run(wet_mode="ru", ru=0.3) line = run( wet_mode="line", lines=[{"from_m": 0, "to_m": 1e6, "points": [[-20.0, 4.0], [20.0, 4.0]]}] ) rock = ground_model._face_water(base, chainage) == "rock" assert kds == (half if rock else legacy) # KDS 기본 = 토사 지표 포화(전과 같음) · 암반 ½ assert half[1] > legacy[1] and line[1] > legacy[1] # 수위가 내려가면 안전율 오름 assert ru[1] != legacy[1] and len({round(v[1], 3) for v in (legacy, half, ru, line)}) == 4 assert legacy[0] == kds[0] == half[0] == ru[0] == line[0] # 건기는 우기 갈래와 무관