303 lines
13 KiB
Python
303 lines
13 KiB
Python
# -*- 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("<svg")]) >= 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] # 건기는 우기 갈래와 무관
|