252 lines
11 KiB
Python
252 lines
11 KiB
Python
"""PLAN 511-B B-4 (요구 320-4) — [저장] 횡단 한 바퀴 파이썬 길 = Node 길(`cross.enforce`) · 형상 얹기 Node.
|
||
|
||
못박는 것
|
||
- 가짜 지형(DTM · 빈칸 = 지표면 끝) · 관 지점(세월교 · 물넘이포장 둘 · 배관) · 구조물(포장 · 맹암거 · 소단 ·
|
||
사토장)로 파이썬 길(`_enforce_on_python` = 설계선 → 표본기 넓힘 → 포장 → 하강)과 Node 길
|
||
(`_enforce_on_node` · TS `B06_Section_Cross_Save.enforceSavedCross`)의 측점 설계 · 넓힌 표본 0 차.
|
||
- 그 사이 넓힘 · 지표면 끝 · 포장 켬 · 끔 · 기울임 · 세월교 하강 · 소단 · 사용자 값 승계가 실제로 일어남.
|
||
- 구조물 정본을 못 읽으면 두 길 다 소단 = 저장분 그대로 · 포장 · 맹암거 구간 없음.
|
||
- DTM 아닌 지표면(tin) = 격자 없음(파이썬 길) · 형상 얹기 = Node(`facility.attach`) 부름.
|
||
|
||
./venv/Scripts/python.exe -m pytest resources/tester/test_511_b4_cross_save_mirror.py -q
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import copy
|
||
import json
|
||
import math
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import numpy as np
|
||
import pytest
|
||
|
||
HERE = Path(__file__).resolve().parent
|
||
ROOT = HERE.parents[1]
|
||
for path in (ROOT, HERE):
|
||
if str(path) not in sys.path:
|
||
sys.path.insert(0, str(path))
|
||
|
||
from test_349_0n_cross_enforce_mirror import PIPES # noqa: E402
|
||
|
||
import B05_Profile.B05_Profile_Engine_Sections as sections_mod # noqa: E402
|
||
import B05_Profile.B05_Profile_Structures_Repository as structures_repo # noqa: E402
|
||
import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402
|
||
import B06_Section.B06_Section_Router_Design_Ranges as ranges_mod # noqa: E402
|
||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
|
||
from B06_Section import B06_Section_Server_Calc_Prebuild as prebuild # noqa: E402
|
||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||
from common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402
|
||
from common_util.common_util_surface_sampler import DtmGridSampler # noqa: E402
|
||
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
|
||
|
||
pytestmark = pytest.mark.skipif(
|
||
not (ROOT / "config" / "node_modules" / "vite").is_dir(), reason="프론트엔드 의존성 미설치"
|
||
)
|
||
|
||
SURFACE = {"method": "dtm", "source_filter": "classification", "smooth": False}
|
||
STRUCTURES = [
|
||
StructureInstance(
|
||
structure_id="pv1", type_id="pavement_concrete", placement="interval", start_m=40, end_m=80
|
||
),
|
||
StructureInstance(
|
||
structure_id="ud1",
|
||
type_id="cross_underdrain",
|
||
placement="point",
|
||
chainage_m=160,
|
||
options={"culvert_slope_pct": 2, "culvert_height_m": 0.5},
|
||
),
|
||
StructureInstance(
|
||
structure_id="bm1",
|
||
type_id="berm",
|
||
placement="interval",
|
||
start_m=180,
|
||
end_m=230,
|
||
options={"width_m": 1.0, "interval_m": 3.0, "slope_deg": 0.0, "depth_m": 1.0},
|
||
),
|
||
StructureInstance(
|
||
structure_id="sp1",
|
||
type_id="spoil_bank",
|
||
placement="site",
|
||
chainage_m=335,
|
||
options={"capacity_m3": 300, "side": "자동(성토 쪽)"},
|
||
),
|
||
]
|
||
OFFSETS = [round(-20 + 0.5 * i, 1) for i in range(81)]
|
||
|
||
|
||
def _plan(chainage: float) -> float:
|
||
return round(100.5 + 0.6 * math.sin(chainage / 53) + 0.002 * chainage, 4)
|
||
|
||
|
||
def _npz(root: Path) -> None:
|
||
"""DTM 격자 — y = 노선 왼쪽(+) · −42 m 아래 빈칸(지표면 끝) · x 180~240 은 −30 m 아래도."""
|
||
models = root / "B04_PreProcess" / "models"
|
||
models.mkdir(parents=True)
|
||
x = np.arange(-10.0, 431.0, 1.0)
|
||
y = np.arange(-80.0, 81.0, 1.0)
|
||
xx, yy = np.meshgrid(x, y)
|
||
# 가로 기울기 0.68 ~ 0.78(반폭 20 m 에서 안 닫히는 측점 · 닫히는 거리 갈림 · 지표면 끝)
|
||
z = 100 + (0.73 + 0.05 * np.sin(xx / 40)) * yy + 0.8 * np.sin(xx / 37) + 0.3 * np.cos(yy / 3)
|
||
valid = ~((yy < -42) | ((xx >= 180) & (xx <= 240) & (yy < -30)))
|
||
np.savez(models / f"dtm_{SURFACE['source_filter']}.npz", x=x, y=y, z=z, valid_mask=valid)
|
||
|
||
|
||
def _sections(root: Path) -> tuple[dict, list[dict]]:
|
||
sampler = DtmGridSampler.from_npz(
|
||
root / "B04_PreProcess" / "models" / f"dtm_{SURFACE['source_filter']}.npz"
|
||
)
|
||
chainages = sorted({20.0 * i for i in range(21)} | {p["chainage_m"] for p in PIPES})
|
||
longitudinal = {
|
||
"design_profiles": [
|
||
{"samples": [{"chainage_m": c, "elevation_m": _plan(c)} for c in chainages]}
|
||
]
|
||
}
|
||
out = []
|
||
for index, c in enumerate(chainages):
|
||
xy = np.array([[c, o] for o in OFFSETS])
|
||
z, _valid = sampler.sample_xy(xy)
|
||
samples = [
|
||
{
|
||
"offset_m": o,
|
||
"x": c,
|
||
"y": o,
|
||
"z": round(float(e), 6),
|
||
"elevation_m": round(float(e), 6),
|
||
"valid": True,
|
||
}
|
||
for o, e in zip(OFFSETS, z)
|
||
]
|
||
# 저장분 = 옛 입력(포장 · 기울임 · 하강은 지금 시설과 다르게) + 사용자 값
|
||
options = dict(
|
||
ground_type="soil" if index % 3 == 0 else "ripping_rock",
|
||
section_mode="left_cut" if index % 2 else "right_cut",
|
||
ditch_type="standard",
|
||
paved=index % 4 == 0,
|
||
standard=STANDARD_CROSS_SECTION,
|
||
surface_drop_m=0.0,
|
||
shoulder_extend_left_m=0.3 if index % 5 == 0 else 0.0,
|
||
)
|
||
design = compute_cross_design(samples, _plan(c), **options)
|
||
design.update(status="provisional", pavement_suggested=False, structure_note="메모")
|
||
if index % 5 == 0:
|
||
design["shoulder_extend_left_m"] = 0.3
|
||
if index % 7 == 0:
|
||
design["cut_slope_ratio_user"] = 0.8
|
||
if index % 6 == 0:
|
||
design["berm"] = {"width_m": 1.5, "interval_m": 5.0, "slope_deg": 0.0, "depth_m": 2.0}
|
||
design["berm_depth_left_m"] = 0.5
|
||
out.append(
|
||
{
|
||
"chainage_m": c,
|
||
"center_x": c,
|
||
"center_y": 0.0,
|
||
"frame": {"origin": {"x": c, "y": 0.0}, "left_xy": [0.0, 1.0]},
|
||
"plan_radius_m": 60.0 if index == 4 else None,
|
||
"curve_outer_side": "left" if index == 4 else None,
|
||
"curve_widening_m": 0.5 if index == 4 else None,
|
||
"samples": samples,
|
||
"design": design,
|
||
}
|
||
)
|
||
return longitudinal, out
|
||
|
||
|
||
def _facilities(monkeypatch, readable: bool = True) -> None:
|
||
pipes = parse_pipe_points(PIPES)
|
||
|
||
def structures(_root):
|
||
if not readable:
|
||
raise OSError("구조물 정본 못 읽음(시험)")
|
||
return 0, STRUCTURES
|
||
|
||
monkeypatch.setattr(sections_mod, "route_pipe_points", lambda *_a, **_k: pipes)
|
||
for module in (structures_repo, ranges_mod, tilt_mod):
|
||
monkeypatch.setattr(module, "load_structures", structures)
|
||
|
||
|
||
def _both(tmp_path: Path, monkeypatch, readable: bool = True):
|
||
root = tmp_path / "p"
|
||
_npz(root)
|
||
_facilities(monkeypatch, readable)
|
||
longitudinal, sections = _sections(root)
|
||
py, node = copy.deepcopy(sections), copy.deepcopy(sections)
|
||
prebuild._enforce_on_python(longitudinal, py, root, None, SURFACE)
|
||
grid = prebuild.design_grid_file(root, SURFACE)
|
||
assert grid is not None and grid.is_file()
|
||
assert prebuild._enforce_on_node(longitudinal, node, root, None, grid)
|
||
return sections, py, node
|
||
|
||
|
||
def _same(py: list[dict], node: list[dict]) -> None:
|
||
"""값 0 차(수는 값으로 — Node 를 거친 저장분 칸은 5.0 이 5 로 옴)."""
|
||
for left, right in zip(py, node, strict=True):
|
||
assert left["samples"] == right["samples"], left["chainage_m"]
|
||
assert left["design"] == right["design"], left["chainage_m"]
|
||
|
||
|
||
def test_저장_횡단_한_바퀴_파이썬_길_Node_길_0차(tmp_path, monkeypatch):
|
||
before, py, node = _both(tmp_path, monkeypatch)
|
||
_same(py, node)
|
||
by = {s["chainage_m"]: s for s in node}
|
||
widened = [s["chainage_m"] for s in node if len(s["samples"]) > len(OFFSETS)]
|
||
assert len(widened) >= 5, widened # 넓힘이 실제로 돎
|
||
assert any(s["design"].get("slope_unclosed") for s in node) # 지표면 끝까지 넓혀도 안 닫힘
|
||
assert (
|
||
by[60.0]["design"]["paved"] is True and by[100.0]["design"]["paved"] is False
|
||
) # 포장 켬 · 끔
|
||
assert by[160.0]["design"]["tilted"] is True # 맹암거 자리 기울임
|
||
assert by[250.0]["design"]["paved"] is True and by[250.0]["design"]["tilted"] is True # 물넘이
|
||
assert by[110.0]["design"]["surface_drop_m"] > 0 # 세월교 하강
|
||
assert by[200.0]["design"]["berm"]["width_m"] == 1.0 # 소단 = 구조물 정본 구간
|
||
assert (
|
||
"berm" not in by[120.0]["design"] or by[120.0]["design"]["berm"] is None
|
||
) # 구간 밖 = 없음
|
||
assert all(s["design"]["structure_note"] == "메모" for s in node) # 사용자 값 승계
|
||
assert by[0.0]["design"]["shoulder_extend_left_m"] == 0.3
|
||
assert any(a["design"] != b["design"] for a, b in zip(before, node))
|
||
|
||
|
||
def test_구조물_정본_못_읽음_소단_저장분_그대로(tmp_path, monkeypatch):
|
||
before, py, node = _both(tmp_path, monkeypatch, readable=False)
|
||
_same(py, node)
|
||
by = {s["chainage_m"]: s for s in node}
|
||
saved = {s["chainage_m"]: s for s in before}
|
||
assert by[0.0]["design"]["berm"] == saved[0.0]["design"]["berm"] # 저장분 소단
|
||
assert by[60.0]["design"]["paved"] is False # 포장 구간 없음(물넘이 밖)
|
||
assert not by[160.0]["design"].get("tilted") # 맹암거 구간 없음(칸 없음)
|
||
|
||
|
||
def test_DTM_아닌_지표면은_격자_없음(tmp_path):
|
||
root = tmp_path / "p"
|
||
_npz(root)
|
||
assert prebuild.design_grid_file(root, {**SURFACE, "method": "tin"}) is None
|
||
assert prebuild.design_grid_file(root, {**SURFACE, "source_filter": "없음"}) is None
|
||
|
||
|
||
def test_형상_얹기는_Node(monkeypatch):
|
||
from B06_Section import B06_Section_Engine_Attach as attach_mod
|
||
from common_util import common_util_calc_runner as runner
|
||
|
||
calls = []
|
||
real = runner.run_calc
|
||
monkeypatch.setattr(
|
||
runner, "run_calc", lambda ids, inputs: calls.append(ids) or real(ids, inputs)
|
||
)
|
||
sections = [
|
||
{"chainage_m": float(c), "center_x": float(c), "center_y": 0.0} for c in range(0, 401, 10)
|
||
]
|
||
misses = attach_mod.attach_shapes(sections, parse_pipe_points(PIPES), STRUCTURES)
|
||
assert calls == [["facility.attach"]]
|
||
assert misses == {"outside": [], "far": []}
|
||
keys = {k for s in sections for k in s} - {"chainage_m", "center_x", "center_y"}
|
||
assert {"ford", "ford_pavement", "culvert", "pavement", "underdrain"} <= keys
|
||
# 파이썬 짝(Node 못 부를 때)과 같음
|
||
py = [
|
||
{"chainage_m": s["chainage_m"], "center_x": s["center_x"], "center_y": 0.0}
|
||
for s in sections
|
||
]
|
||
attach_mod._attach_python(py, parse_pipe_points(PIPES), STRUCTURES)
|
||
assert json.loads(json.dumps(py)) == json.loads(json.dumps(sections))
|