Files
Aislo/resources/tester/test_532_4_stab_walls_water.py
T
eomsangdonandClaude Opus 5.5 f341e0465f feat(E03): 바닥이 벽(옹벽 · 석축) 안을 지나는 후보 활동면 버림 (532-2 2차 ⑤)
- LEM 절편: 바닥 물성 = 벽 인 절편이 하나라도 있으면 그 면 무효 · 벽 밑으로 도는 면만 후보
- 시험: 벽 한가운데 지나는 꺾은선 = 값 없음 · 벽 바닥 아래 꺾은선 = 값 있음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 03:26:18 +09:00

309 lines
13 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 -*-
"""532-4 E03 벽(옹벽 · 석축 · 돌쌓기) · 532-3 덧 E02 지하수 갈래.
- 벽 = B06 화면 TS(`computeStoredLayouts`)를 서버 Node 묶음(`build:e03-walls`)으로 —
벽 선 측점 임계면이 벽 폴리곤을 안 지남(모서리 스침 ≤ 0.2 m · 절편 폭 아래) ·
벽 없는 측점 = 종전 그대로 · 복사본(구조물 정본 → B06 얹기) 끝까지.
- 지하수 = 건기 · 우기 갈래 넷(KDS · 깊이 비율 · r_u · 사용자 수위선) → 엔진 꼴 ·
[저장] · [초기화] · 옛 정본 채움.
./venv/Scripts/python.exe -m pytest resources/tester/test_532_4_stab_walls_water.py -q
"""
from __future__ import annotations
import asyncio
import copy
import json
import sys
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 E02_StabGround import E02_StabGround_Model as ground_model # noqa: E402
from E02_StabGround import E02_StabGround_Router as router # noqa: E402
from E03_StabSection import E03_StabSection_Types as types # noqa: E402
from E03_StabSection.E03_StabSection_Engine_Model import section_model_from_b06 # noqa: E402
from E03_StabSection.E03_StabSection_Engine_Search import analyze, evaluate_specified # noqa: E402
from E03_StabSection.E03_StabSection_Engine_Station import analyze_station # noqa: E402
from E03_StabSection.E03_StabSection_Walls import ( # noqa: E402
design_with_walls,
walls_by_station,
walls_for_sections,
walls_of,
)
# 오른쪽(−) 산 · 왼쪽(+) 골 · 노면 −2~2 · 좌 성토 1:1.5(test_532_2 와 같은 단면)
GROUND = [(x / 2, 10.0 - 0.6 * (x / 2)) for x in range(-40, 41)]
LINE = [(-20.0, 22.0), (-6.0, 13.6), (-2.6, 10.0), (-2.0, 10.0), (2.0, 10.0), (14.0, 2.0)]
DESIGN = {
"section_mode": "right_cut",
"ground_type": "soil",
"road_edges": {
"left": {"offset_m": 2.0, "elevation_m": 10.0},
"right": {"offset_m": -2.0, "elevation_m": 10.0},
},
"carriageway_edges": {
"left": {"offset_m": 1.5, "elevation_m": 10.0},
"right": {"offset_m": -1.5, "elevation_m": 10.0},
},
"cross_slope_pct": 0,
"design_elevation_m": 10.0,
"design_line": [{"offset_m": a, "elevation_m": b} for a, b in LINE],
}
CROSS = {
"chainage_m": 100.0,
"label": "STA.0+100",
"samples": [{"offset_m": a, "elevation_m": b, "valid": True} for a, b in GROUND],
"design": DESIGN,
}
WALL = {
"structure_id": "w1",
"type_id": "masonry_wet",
"name": "돌쌓기(찰)",
"start_m": 90.0,
"end_m": 110.0,
"anchor_m": 100.0,
"form": "돌쌓기(찰)",
"height_m": 3.0,
"tiers": 1,
"extras": [],
}
FAST = {"conditions": ("dry",), "methods": ("bishop", "janbu")}
CORNER_M = 0.2 # 임계면이 벽 모서리를 스치는 길이 한도(절편 폭 아래)
@pytest.fixture(scope="module")
def found():
return walls_for_sections([{**CROSS, "revetment": WALL}, {**CROSS, "chainage_m": 120.0}])
def test_벽_도형_B06_산식(found):
assert list(found) == ["100.00"] # 벽 없는 측점은 안 실음
entry = found["100.00"]
(wall,) = entry["walls"]
assert wall["material_id"] == "wall" and wall["kind"] == "own"
poly = Polygon(wall["polygon"])
assert poly.is_valid and len(wall["polygon"]) == 4
# 높이 = 바닥(근입 0.5 포함) ~ 상단 3 m · 상단 = 노견 높이 · 성토 쪽(좌 +)
zs = [p[1] for p in wall["polygon"]]
assert max(zs) - min(zs) == pytest.approx(3.0) and max(zs) == pytest.approx(10.0)
assert min(p[0] for p in wall["polygon"]) > 0
# 지표 = 그려지는 설계선 — 벽 상단(노견)에서 끊겨 바깥은 원지반
line = entry["design_line"]
gx, gz = zip(*GROUND)
out = [(x, z) for x, z in line if x > 2.0 + 1e-3]
assert out and all(abs(z - np.interp(x, gx, gz)) < 1e-6 for x, z in out)
def test_임계면이_벽을_안_지남(found):
entry = found["100.00"]
design = design_with_walls(DESIGN, entry)
res = analyze_station(CROSS, design, walls=walls_of(entry), **FAST)
poly = Polygon(entry["walls"][0]["polygon"])
fill = [c for c in res["cases"] if c["role"] == "fill"]
assert fill
for case in fill:
for got in case["result"]["methods"].values():
assert got["min"]["F"] > 0
assert LineString(got["min"]["points"]).intersection(poly).length <= CORNER_M
plain = analyze_station(CROSS, DESIGN, **FAST)
# 벽이 성토 비탈을 대신함 — 성토 쪽 최소 F 가 달라짐 · 절토 쪽은 그대로
for a, b in zip(plain["cases"], res["cases"]):
if a["role"] == "cut":
assert a["min_F"] == pytest.approx(b["min_F"], abs=1e-9)
else:
assert a["min_F"]["bishop"] != pytest.approx(b["min_F"]["bishop"], abs=1e-3)
def test_벽을_지나는_면은_버림(found):
entry = found["100.00"]
model = section_model_from_b06(CROSS, design_with_walls(DESIGN, entry), walls=walls_of(entry))
side = next(s for s in model["sides"] if s["role"] == "fill")
through = [[-0.5, 10.0], [2.0, 8.5], [6.0, 6.4]] # 벽(x 1.4~3.2 · z 7~10) 한가운데
under = [[-0.5, 10.0], [2.0, 6.0], [8.0, 5.2]] # 벽 바닥 아래
for line, ok in ((through, False), (under, True)):
got = evaluate_specified(
model, polyline=line, direction=side["direction"], methods=("janbu", "spencer")
)
assert all((got[m]["F"] is not None) == ok for m in got)
def test_벽_없는_측점_모형_그대로():
assert design_with_walls(DESIGN, None) is DESIGN and walls_of(None) is None
a = section_model_from_b06(CROSS, DESIGN)
b = section_model_from_b06(CROSS, design_with_walls(DESIGN, None), walls=walls_of(None))
assert json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)
def _write_copy(root: Path, with_wall: bool) -> Path:
"""E01 동결 복사본 꼴 — db.json(측점 행 · 노선) · 횡단 파일 · 구조물 정본."""
from B05_Profile.B05_Profile_Structures_Repository import save_structures
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
source = root / "E01_StabRegister" / "source"
(source / "B06_Section" / "cross_sections").mkdir(parents=True)
(source / "copy_manifest.json").write_text("{}", encoding="utf-8")
rows = []
for k, chainage in enumerate((80.0, 100.0)):
rel = f"B06_Section/cross_sections/cross_{int(chainage):06d}.json"
cross = {k: v for k, v in CROSS.items() if k != "design"}
cross.update(chainage_m=chainage, center_x=float(chainage), center_y=0.0)
(source / rel).write_text(json.dumps(cross), encoding="utf-8")
rows.append(
{
"chainage_m": chainage,
"sequence_num": k,
"data": json.dumps({"design": DESIGN}),
"cross_section_file_path": rel,
}
)
db = {"cross_sections": rows, "route": {"route_data_path": None}}
(source / "db.json").write_text(json.dumps(db), encoding="utf-8")
if with_wall:
item = StructureInstance(
type_id="masonry_wet",
placement="interval",
chainage_m=100,
start_m=95,
end_m=105,
options={"height_m": 3.0},
)
save_structures(str(source), [item], base_revision=0)
return source
def test_복사본_구조물_정본에서_측점별_벽(tmp_path):
source = _write_copy(tmp_path / "a", with_wall=True)
got = walls_by_station(source)
assert list(got) == ["100.00"]
(wall,) = got["100.00"]["walls"]
assert wall["name"] == "돌쌓기(찰)" and len(wall["polygon"]) == 4
assert walls_by_station(_write_copy(tmp_path / "b", with_wall=False)) == {}
# ---- 지하수 갈래 -----------------------------------------------------------------
STATIONS = [
{"chainage_m": 0.0, "ground_type": "soil", "soil_thickness_m": None},
{"chainage_m": 20.0, "ground_type": "blasting_rock", "soil_thickness_m": 0.0},
]
SURFACE = [[-10.0, 16.0], [-2.0, 10.0], [2.0, 10.0], [10.0, 4.0]]
HEIGHTS = {"left": 6.0, "right": 4.0}
def _water(chainage=0.0, condition="wet", **groundwater):
ground = ground_model.build_initial(ground_model.load_defaults(), copy.deepcopy(STATIONS))
ground["groundwater"].update(groundwater)
out = ground_model.groundwater_for(
ground, condition, chainage_m=chainage, surface=SURFACE, heights=HEIGHTS
)
assert types.groundwater(out)["mode"] == out["mode"] # 엔진 꼴 그대로
return out
def _depth(out):
sx, sz = zip(*SURFACE)
return {round(x, 6): round(float(np.interp(x, sx, sz)) - z, 6) for x, z in out["line"]}
def test_지하수_건기_KDS():
assert _water(condition="dry") == {"mode": "none"}
assert _water(condition="seismic") == {"mode": "none"}
assert set(_depth(_water()).values()) == {0.0} # 토사 측점 = 지표 포화
rock = _depth(_water(chainage=20.0)) # 연암(겉 = 암 · 두께 0) = 비탈 높이 ½
assert rock[10.0] == 3.0 and rock[-10.0] == 2.0
def test_지하수_깊이비율_ru_수위선():
ratio = _depth(_water(wet_mode="depth_ratio", depth_ratio=0.25))
assert ratio[10.0] == 1.5 and ratio[-10.0] == 1.0 and ratio[2.0] == 1.5
assert _water(wet_mode="ru", ru=0.3) == {"mode": "ru", "ru": 0.3}
lines = [
{"from_m": 0.0, "to_m": 10.0, "points": [[-5.0, 2.0], [5.0, 1.0]]},
{"from_m": 0.0, "to_m": 5.0, "points": [[-5.0, 3.0], [5.0, 3.0]]}, # 뒤 구간이 이김
]
user = _depth(_water(wet_mode="line", lines=lines))
assert set(user.values()) == {3.0}
one = _depth(_water(wet_mode="line", lines=lines[:1]))
assert one[-10.0] == 2.0 and one[2.0] == 1.3 and one[10.0] == 1.0 # 끝은 끝값
# 덮지 않은 측점 = KDS
assert set(_depth(_water(chainage=20.0, wet_mode="line", lines=lines)).values()) == {2.0, 3.0}
def test_지하수_측점_모형에_끼움():
model = section_model_from_b06(CROSS, DESIGN, wet=True)
ground = ground_model.build_initial(ground_model.load_defaults(), [])
ground["groundwater"].update(wet_mode="depth_ratio", depth_ratio=0.5)
gw = ground_model.station_groundwater(ground, "wet", model)
left = next(s["height_m"] for s in model["sides"] if s["side"] == "left")
sx, sz = zip(*model["surface"])
x, z = next(p for p in gw["line"] if p[0] > 1)
assert float(np.interp(x, sx, sz)) - z == pytest.approx(0.5 * left)
model["groundwater"] = types.groundwater(gw)
side = next(s for s in model["sides"] if s["side"] == "left")
res = analyze(model, direction=side["direction"], limits=side["limits"], methods=("bishop",))
assert res["methods"]["bishop"]["min"]["F"] > 0 # 끼운 모형이 엔진에서 돎
def test_측점_해석_groundwater_for_인자():
ground = ground_model.build_initial(ground_model.load_defaults(), [])
ground["groundwater"].update(wet_mode="ru", ru=0.3)
seen = []
def gw_for(cond, model):
seen.append(cond)
return ground_model.station_groundwater(ground, cond, model)
kw = {"conditions": ("dry", "wet"), "methods": ("bishop",), "noncircular": False}
base = analyze_station(CROSS, DESIGN, **kw)
out = analyze_station(CROSS, DESIGN, groundwater_for=gw_for, **kw)
assert seen == ["dry", "wet"]
F = {(c["condition"], c["side"]): c["min_F"]["bishop"] for c in out["cases"]}
F0 = {(c["condition"], c["side"]): c["min_F"]["bishop"] for c in base["cases"]}
for (cond, side), f in F.items():
if cond == "dry":
assert f == pytest.approx(F0[cond, side]) # 건기 = 수위 없음 그대로
else:
assert f > F0[cond, side] # r_u 0.3 < 지표 포화
@pytest.fixture
def project(tmp_path, monkeypatch):
async def fake_project(_project_id):
return tmp_path, {"company_id": 1, "project_kind": "STABILITY"}
monkeypatch.setattr(router, "_project", fake_project)
monkeypatch.setattr(router, "_b06_stations", lambda _root: copy.deepcopy(STATIONS))
return tmp_path
def test_지하수_저장_초기화_옛정본(project):
pid = UUID("9872e9a7-e483-427b-a6b2-72cdbc063bc3")
session = {"role": "USER", "company_id": 1}
first = asyncio.run(router.get_ground(pid, session))
ground = copy.deepcopy(first["ground"])
assert ground["groundwater"]["wet_mode"] == "kds"
line = {"from_m": 0.0, "to_m": 20.0, "points": [[10.0, 2.0], [-10.0, 1.0]][::-1]}
ground["groundwater"].update(wet_mode="line", lines=[line], ru=0.35)
asyncio.run(router.put_ground(pid, router.GroundRequest(ground=ground)))
stored = json.loads(ground_model.ground_path(project).read_text(encoding="utf-8"))
assert stored["groundwater"] == {**ground["groundwater"], "lines": [line]}
bad = copy.deepcopy(ground)
bad["groundwater"]["lines"][0]["points"] = [[1.0, 1.0]]
with pytest.raises(router.HTTPException):
asyncio.run(router.put_ground(pid, router.GroundRequest(ground=bad)))
back = asyncio.run(router.reset_ground(pid))["ground"]
assert back["groundwater"]["wet_mode"] == "kds" and back["groundwater"]["lines"] == []
# 옛 정본(갈래 칸 없음) = GET 이 기본값으로 채움 · 있는 칸은 그대로
old = json.loads(ground_model.ground_path(project).read_text(encoding="utf-8"))
old["groundwater"] = {"dry": "none", "wet_soil": "none", "wet_rock": "half_height"}
ground_model.ground_path(project).write_text(json.dumps(old), encoding="utf-8")
got = asyncio.run(router.get_ground(pid, session))["ground"]["groundwater"]
assert got["wet_soil"] == "none" and got["wet_mode"] == "kds" and got["lines"] == []