feat(B04·B05 배수): 369-1 관 자리 기본 = 세류선 교차만 · auto 칸(all · add) · 노선 저장 오목 보충 끔
- build_detail auto — 없음 = 세류선 교차만 · all = 세류선 + 100 m 보충으로 다시 · add = 지금 목록 + 빈 구간 보충 · 후보는 늘 셈 - 배수유역 라우터 — 요청 auto 칸(틀리면 400) · 같은 자리(0.05 m) 관만 속성 · id · 사유 이어받음 · 나머지 새 관 = 등록부 기본값 · 저장본 열쇠에 auto - 노선 [저장] · 추천 [적용] — 오목 지점 보충 지움(응답 concave 칸 · 화면 한 줄 같이) - 시험 — test_369_drainage_auto 세 모드 · test_346_3 · 312 · 342_3 고쳐 씀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
@@ -56,7 +56,7 @@ const STUBS = {
|
||||
if (failReplan) throw new Error("체인 실패");
|
||||
return {
|
||||
route_id: 9,
|
||||
pipes: { kept: 3, crossing: 1, concave: 2 },
|
||||
pipes: { kept: 3, crossing: 1 },
|
||||
structures: { moved: 1, far: 0 },
|
||||
cross_dropped: 1,
|
||||
drainage_stale: true,
|
||||
|
||||
@@ -148,7 +148,7 @@ def test_칸_없음_파일_없음_DELETE_노선_바뀜은_자동_배치(api) ->
|
||||
|
||||
def test_빈_목록과_None_을_가르는_자리() -> None:
|
||||
src = _read("common_util/common_util_drainage_detail.py")
|
||||
assert "if confirmed_chainages is not None:" in src
|
||||
assert 'if confirmed_chainages is not None and auto != "all":' in src
|
||||
router = _read("B04_PreProcess/B04_PreProcess_Router_Basins.py")
|
||||
assert "points = requested if requested is not None else stored" in router
|
||||
assert "requested or stored" not in router
|
||||
|
||||
@@ -24,7 +24,7 @@ def setup(tmp_path, monkeypatch):
|
||||
basins._DETAIL_CACHE.clear()
|
||||
calls = SimpleNamespace(count=0)
|
||||
|
||||
def build(stored_path, context, points):
|
||||
def build(stored_path, context, points, auto=None):
|
||||
calls.count += 1
|
||||
chainages = [p.chainage_m for p in points] if points else [50.0]
|
||||
return DrainageDetail(), [PipePoint(chainage_m=c, source="stream") for c in chainages]
|
||||
|
||||
@@ -52,13 +52,13 @@ def _patch(monkeypatch, analysis_signature, stored):
|
||||
monkeypatch.setattr(basins, "load_pipe_points", lambda *args: stored)
|
||||
built = SimpleNamespace(called=False)
|
||||
|
||||
def build(stored_path, context, points):
|
||||
def build(stored_path, context, points, auto=None):
|
||||
built.called = True
|
||||
return SimpleNamespace(pipes=[], basins=["유역"]), points or []
|
||||
|
||||
monkeypatch.setattr(basins, "_build", build)
|
||||
monkeypatch.setattr(
|
||||
basins, "_build_cached", lambda context, points: build("x", context, points)
|
||||
basins, "_build_cached", lambda context, points, auto=None: build("x", context, points)
|
||||
)
|
||||
return built
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""346-3 노선 저장 서버 길 — 차례 · 관 지킴 · 새 교차점 기본 관 · 오목 보충 · 이월 열쇠 ·
|
||||
"""346-3 노선 저장 서버 길 — 차례 · 관 지킴 · 새 교차점 기본 관 · 보충 없음(369) · 이월 열쇠 ·
|
||||
초기값 규칙 ·
|
||||
배수유역 낡음 · 실패 때 옛 노선 되살림.
|
||||
|
||||
@@ -41,14 +41,11 @@ import common_util.common_util_workflow_state as workflow # noqa: E402
|
||||
from B05_Profile.B05_Profile_Route_Save_Parts import ( # noqa: E402
|
||||
add_crossing_pipes,
|
||||
carry_cross_inputs,
|
||||
concave_chainages,
|
||||
fill_concave_pipes,
|
||||
follow_pipes,
|
||||
station_keys,
|
||||
vertices_of,
|
||||
)
|
||||
from common_util.common_util_drainage_pipes import ( # noqa: E402
|
||||
PIPE_SOURCE_SPACING,
|
||||
PIPE_SOURCE_STREAM,
|
||||
PipePoint,
|
||||
analysis_route_signature,
|
||||
@@ -60,7 +57,7 @@ OLD = [(0.0, 0.0), (100.0, 0.0), (200.0, 0.0), (300.0, 0.0), (400.0, 0.0)]
|
||||
NEW = [(0.0, 0.0), (100.0, 0.0), (200.0, 60.0), (300.0, 0.0), (400.0, 0.0)]
|
||||
|
||||
|
||||
# ── 관 따라감 · 교차점 · 오목 보충 ─────────────────────────────────────────────
|
||||
# ── 관 따라감 · 교차점 ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_관은_지우지_않고_좌표를_따라감():
|
||||
@@ -91,29 +88,11 @@ def test_새_교차점에만_기본_관():
|
||||
assert add_crossing_pipes(pairs, crossings, OLD, 456.0) == 0
|
||||
|
||||
|
||||
def test_오목_지점_보충은_새_자리의_오목만():
|
||||
vertices = vertices_of(NEW)
|
||||
total = vertices[-1].chainage_m
|
||||
chainage = np.arange(0.0, total, 2.0)
|
||||
ground = 100.0 + np.abs(chainage - 220.0) * 0.05 # 220 m 에 골(새 자리)
|
||||
ground[chainage < 60] = (
|
||||
100.0 + np.abs(chainage[chainage < 60] - 30.0) * 0.05
|
||||
) # 30 m 골(옛 자리)
|
||||
concave = concave_chainages(chainage, ground)
|
||||
assert any(abs(at - 220.0) < 3 for at, _ in concave)
|
||||
pairs: list = []
|
||||
added = fill_concave_pipes(pairs, concave, vertices, OLD)
|
||||
places = [pipe.chainage_m for _, pipe in pairs]
|
||||
assert added >= 1 and all(pipe.source == PIPE_SOURCE_SPACING for _, pipe in pairs)
|
||||
assert any(abs(at - 220.0) <= 3 for at in places)
|
||||
assert not any(at < 60 for at in places) # 옛 노선 위 오목엔 안 둠
|
||||
def test_노선_저장은_간격_보충_없음():
|
||||
"""369 — 오목 · 100 m 간격 보충은 배수유역 [자동 제안]만 · 노선 저장 부품에 없음."""
|
||||
import B05_Profile.B05_Profile_Route_Save_Parts as parts
|
||||
|
||||
|
||||
def test_간격이_좁으면_보충_없음():
|
||||
vertices = vertices_of(NEW)
|
||||
pairs = [(0, PipePoint(chainage_m=at)) for at in (0.0, 90.0, 180.0, 270.0, 360.0, 450.0)]
|
||||
concave = [(220.0, 99.0)]
|
||||
assert fill_concave_pipes(pairs, concave, vertices, OLD) == 0
|
||||
assert not hasattr(parts, "fill_concave_pipes") and not hasattr(save, "_ground_along")
|
||||
|
||||
|
||||
# ── 측점 열쇠 · 횡단 이월 ──────────────────────────────────────────────────────
|
||||
@@ -366,7 +345,6 @@ def chain_world(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(watershed, "_prepare", prepare)
|
||||
monkeypatch.setattr(drainage_pipes, "clear_pipe_points", watershed_must_not_run)
|
||||
monkeypatch.setattr(drainage_pipes, "drainage_dir", lambda stored: drain)
|
||||
monkeypatch.setattr(save, "_ground_along", lambda *a: (np.zeros(0), np.zeros(0)))
|
||||
monkeypatch.setattr(b06_router, "regenerate_route_sections", regenerate)
|
||||
monkeypatch.setattr(save, "_carry_profile", _async(None, order, "profile"))
|
||||
monkeypatch.setattr(save, "_carry_cross", _async(2, order, "cross"))
|
||||
@@ -416,7 +394,8 @@ def test_차례와_배수유역_뗌(chain_world):
|
||||
"confirm",
|
||||
]
|
||||
assert result["cross_dropped"] == 2 and result["drainage_stale"] is True
|
||||
assert result["pipes"] == {"kept": 1, "crossing": 0, "concave": 0}
|
||||
# 456 m 노선 · 관 하나 — 100 m 간격 보충 없음(369)
|
||||
assert result["pipes"] == {"kept": 1, "crossing": 0}
|
||||
# 관 정본 = 새 노선 지문 · 배수유역 분석 지문(옛) 과 달라 B04 가 「낡음」
|
||||
saved = json.loads(chain_world.pipes_file.read_text("utf-8"))
|
||||
assert saved["route_signature"] == route_signature(vertices_of(NEW))
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_저장_뒤_가게_초안_비움_결과_한_줄(out):
|
||||
assert out["storeAfterSave"] is None
|
||||
assert out["keptAfterSave"] == 1
|
||||
assert out["summary"] == (
|
||||
"노선 저장 — 관 지킴 3 · 새 교차 관 1 · 오목 보충 관 2 · 구조물 옮김 1 · 멀어짐 0 · "
|
||||
"노선 저장 — 관 지킴 3 · 새 교차 관 1 · 구조물 옮김 1 · 멀어짐 0 · "
|
||||
"노선 밖 고침 3 건 · 배수유역 낡음"
|
||||
) # 서버 cross_dropped 1 + 화면 넘김 밖 2 = 한 줄
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""369 배수 구조물 자리 — 기본 = 세류선 교차만 · [자동 제안] 두 방식(all · add).
|
||||
|
||||
- 기본(auto 없음) = 세류선 교차 관만 · 100 m 보충 없음 · 관 자리 후보는 셈.
|
||||
- all = 세류선 + 100 m 보충으로 다시(지금 목록 무시) · 같은 자리(0.05 m) 관은 속성 · id 유지 ·
|
||||
나머지 = 새 관(등록부 기본값).
|
||||
- add = 지금 목록 그대로 + 빈 구간 보충 · 더한 관 = 새 관(등록부 기본값).
|
||||
- 틀린 auto = 400 · 저장본 열쇠에 auto.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import numpy as np
|
||||
|
||||
from B04_PreProcess import B04_PreProcess_Router_Basins as basins
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Grid import GridSpec
|
||||
from common_util.common_util_drainage_context import DrainageContext
|
||||
from common_util.common_util_drainage_detail import RoadRouting, build_detail
|
||||
from common_util.common_util_drainage_pipes import PipePoint
|
||||
from common_util.common_util_route_geometry import RouteVertex, StructureCandidate
|
||||
|
||||
VERTICES = [RouteVertex(0.0, 0.0, 100.0, 0.0), RouteVertex(300.0, 0.0, 90.0, 300.0)]
|
||||
CONTEXT = DrainageContext(stored_path="x", project_root=Path("."), vertices=VERTICES)
|
||||
|
||||
|
||||
def _routing() -> RoadRouting:
|
||||
spec = GridSpec(x_min=-5.0, y_max=5.0, cell_m=1.0, n_rows=10, n_cols=310)
|
||||
road_index = np.arange(5 * 310 + 5, 5 * 310 + 305, dtype=np.int32)
|
||||
road_slot = np.full(spec.size, -1, dtype=np.int32)
|
||||
road_slot[road_index] = np.arange(road_index.size)
|
||||
return RoadRouting(
|
||||
spec=spec,
|
||||
road_slot=road_slot,
|
||||
path_length=np.zeros(spec.size, np.float32),
|
||||
elevation=np.full(spec.size, 95.0, np.float32),
|
||||
road_cell_index=road_index,
|
||||
road_chainage=np.arange(road_index.size, dtype=np.float64),
|
||||
strength=np.ones(road_index.size, np.int64),
|
||||
base_pipes=[StructureCandidate(chainage_m=150.0, x=150.0, y=0.0, reason="stream")],
|
||||
)
|
||||
|
||||
|
||||
def _detail(tmp_path, chainages=None, auto=None):
|
||||
return build_detail(tmp_path / "없음", VERTICES, chainages, _routing(), [], auto)
|
||||
|
||||
|
||||
def test_기본은_세류선_교차만(tmp_path):
|
||||
detail = _detail(tmp_path)
|
||||
assert [(p.chainage_m, p.reason) for p in detail.pipes] == [(150.0, "stream")]
|
||||
assert len(detail.pipe_candidates) == 2 # 0~150 · 150~300 이 100 m 넘음 — 제안만
|
||||
|
||||
|
||||
def test_all_은_세류선_더하기_100m_보충_지금_목록_무시(tmp_path):
|
||||
detail = _detail(tmp_path, [10.0, 150.02], "all")
|
||||
reasons = [p.reason for p in detail.pipes]
|
||||
places = [p.chainage_m for p in detail.pipes]
|
||||
assert reasons.count("stream") == 1 and reasons.count("spacing") == 2
|
||||
assert 150.0 in places and 10.0 not in places
|
||||
gaps = np.diff([0.0, *places, 300.0])
|
||||
assert gaps.max() <= 100.0 and not detail.pipe_candidates
|
||||
|
||||
|
||||
def test_add_는_지금_목록_더하기_빈_구간_보충(tmp_path):
|
||||
detail = _detail(tmp_path, [20.0], "add")
|
||||
places = [p.chainage_m for p in detail.pipes]
|
||||
assert 20.0 in places and len(places) == 3 # 20 ~ 300 = 280 m → 보충 둘
|
||||
assert np.diff([0.0, *places, 300.0]).max() <= 100.0
|
||||
# auto 없음 = 목록 그대로(보충은 후보만)
|
||||
plain = _detail(tmp_path, [20.0])
|
||||
assert [p.chainage_m for p in plain.pipes] == [20.0] and len(plain.pipe_candidates) == 2
|
||||
|
||||
|
||||
def _build(tmp_path, monkeypatch, points, auto):
|
||||
monkeypatch.setattr(basins, "drainage_dir", lambda stored: tmp_path / "없음")
|
||||
return basins._build("x", CONTEXT, points, _routing(), [], auto=auto)
|
||||
|
||||
|
||||
def test_all_같은_자리_관은_속성_id_유지_새_관은_기본값(tmp_path, monkeypatch):
|
||||
kept = PipePoint(
|
||||
chainage_m=150.03,
|
||||
source="user",
|
||||
facility="ford_bridge",
|
||||
options={"ford_width_m": 4.0},
|
||||
id="keep",
|
||||
)
|
||||
gone = PipePoint(chainage_m=10.0, source="user", options={"pipe_diameter_mm": 600}, id="gone")
|
||||
_, carried = _build(tmp_path, monkeypatch, [gone, kept], "all")
|
||||
ford = [p for p in carried if p.id == "keep"]
|
||||
assert len(ford) == 1 and ford[0].facility == "ford_bridge" and ford[0].source == "user"
|
||||
assert all(p.id != "gone" for p in carried)
|
||||
fresh = [p for p in carried if p.id != "keep"]
|
||||
assert len(fresh) == 2 and all(p.source == "spacing" for p in fresh)
|
||||
assert all((p.options or {}).get("pipe_diameter_mm") for p in fresh) # 등록부 관경
|
||||
|
||||
|
||||
def test_add_더한_관은_기본값_지금_관은_그대로(tmp_path, monkeypatch):
|
||||
mine = PipePoint(chainage_m=20.0, source="user", id="mine")
|
||||
_, carried = _build(tmp_path, monkeypatch, [mine], "add")
|
||||
assert [p.id for p in carried].count("mine") == 1
|
||||
assert next(p for p in carried if p.id == "mine").options is None # 옛 빈칸 = 미확정 유지
|
||||
added = [p for p in carried if p.id != "mine"]
|
||||
assert len(added) == 2 and all(p.source == "spacing" for p in added)
|
||||
assert all((p.options or {}).get("pipe_diameter_mm") for p in added)
|
||||
|
||||
|
||||
def test_틀린_auto_는_400_열쇠에_auto(tmp_path, monkeypatch):
|
||||
async def load(project_id):
|
||||
return CONTEXT, ""
|
||||
|
||||
monkeypatch.setattr(basins, "load_drainage_context", load)
|
||||
monkeypatch.setattr(basins, "drainage_dir", lambda stored: tmp_path)
|
||||
result = asyncio.run(basins._resolve(uuid4(), {"auto": "spacing"}, use_stored=False))
|
||||
assert result.status_code == 400
|
||||
points = [PipePoint(chainage_m=20.0)]
|
||||
keys = {basins._detail_key(CONTEXT, points, auto) for auto in basins.AUTO_MODES}
|
||||
assert len(keys) == 3
|
||||
Reference in New Issue
Block a user