Files
Aislo/resources/tester/test_511_b4_cross_save_mirror.py
T

252 lines
11 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.
"""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))