Files
Aislo/resources/tester/test_347_2_drainage_live.py
T
eomsangdonandClaude Opus 5.5 d5d6415966 feat(배수유역): 347-2 「지금 노선으로 셈」 서버 길 · 347-3 관 자리 후보 · [확정] 다시 셈
- POST drainage/live — 몸통 = 차례 번호 + 노선 초안(RouteDraft · 노선 [확인]과 같은 폴리라인 규칙) + 관 목록 · 서버 단독 · 작업본(00_watershed_response · 격자 배열 · 관 지점)에 안 씀 · 세부 유역까지 메모리 격자로 한 요청
- 프로젝트별 마지막 차례 · 1차 영역 뒤 · 확장 회차마다 · 세부 유역 앞에서 바뀌었으면 그만둠(superseded · AnalysisStopped)
- build_detail 에 메모리 routing · 상류선 · 관 자리 후보 suggest_pipe_positions(간격 보충 규칙 · 흐름 세기 + 오목 · 기존 관에서 먼 곳 · 정수 m) · 응답 pipe_candidates
- PUT pipe-points ?recompute=true([확정]) = 낡았으면 배수유역부터 다시 셈 · [저장]은 낡음 달고 관만
- 시험 test_347_2_drainage_live(늦은 차례 그만둠 둘 · 엔진 회차 그만둠 · 메모리 격자 · 후보 · 확정 다시 셈) · 검증_sub3 실측 저장 노선 16.9 s · 초안 노선 11.5 s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
2026-10-16 11:38:17 +09:00

170 lines
6.4 KiB
Python

"""347-2 「지금 노선으로 셈」 서버 길 · 347-3 관 자리 후보.
- 늦은 차례: 계산 중 새 차례가 오면 단계 사이에서 그만둠(superseded) · 작업본에 안 씀.
- 메모리 격자로 세부 유역(파일 안 읽음) · 노선 초안 → 그린 노선.
- 관 자리 후보: 최대 간격(100 m)을 넘는 구간에만 · 기존 관 자리엔 안 냄 · 관 목록엔 안 넣음.
- [확정](recompute) = 낡았으면 배수유역부터 다시 셈 · [저장] = 안 셈.
"""
import asyncio
import math
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4
import numpy as np
import pytest
from shapely.geometry import LineString
from B04_PreProcess import B04_PreProcess_Engine_Watershed_Expand as expand
from B04_PreProcess import B04_PreProcess_Router_Basins as basins
from B04_PreProcess import B04_PreProcess_Router_Watershed_Live as live
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,
pipes_from_chainages,
suggest_pipe_positions,
)
from common_util.common_util_route_geometry import RouteVertex
VERTICES = [RouteVertex(0.0, 0.0, 100.0, 0.0), RouteVertex(300.0, 0.0, 90.0, 300.0)]
def test_draft_vertices_follow_nodes():
draft = {"nodes": [[0, 0], [100, 0], [100, 100]], "curveOn": [False, False, False]}
vertices = live.draft_vertices(draft, 30.0)
assert vertices[0].x == 0 and vertices[-1].y == 100
assert math.isclose(vertices[-1].chainage_m, 200.0)
def test_candidates_only_in_long_gaps():
strength = np.zeros(301)
strength[150] = 50.0 # 흐름이 모이는 자리
pipes = pipes_from_chainages(VERTICES, [0.0, 300.0])
picked = suggest_pipe_positions(VERTICES, pipes, strength)
assert picked and all(0 < pipe.chainage_m < 300 for pipe in picked)
assert all(pipe.chainage_m == round(pipe.chainage_m) for pipe in picked) # 정수 m
dense = pipes_from_chainages(VERTICES, [0.0, 90.0, 180.0, 270.0, 300.0])
assert suggest_pipe_positions(VERTICES, dense, strength) == []
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),
)
def test_build_detail_uses_memory_routing(tmp_path):
missing = tmp_path / "없는 폴더" # 파일을 읽으면 None 이 됨
detail = build_detail(missing, VERTICES, [0.0, 300.0], _routing(), [])
assert detail is not None and detail.pipe_candidates
assert build_detail(missing, VERTICES, [0.0, 300.0]) is None
def _patch_live(monkeypatch, preview_fn):
async def prepare(project_id):
return {"contours": [], "streams": [], "contour_key": "k"}
async def load(project_id):
return DrainageContext(stored_path="x", project_root=Path("."), vertices=VERTICES), ""
monkeypatch.setattr(live, "_prepare", prepare)
monkeypatch.setattr(live, "load_drainage_context", load)
monkeypatch.setattr(live, "drainage_dir", lambda stored: Path("."))
monkeypatch.setattr(live, "preview_stages", preview_fn)
calls = SimpleNamespace(build=0)
def build(*args):
calls.build += 1
return None, []
monkeypatch.setattr(live, "_build", build)
return calls
def test_newer_generation_stops_between_stages(monkeypatch):
pid = uuid4()
def preview(vertices, contours, streams, store, should_stop):
live._LATEST_GENERATION[str(pid)] = 8 # 계산 중 새 조작 → 새 요청이 차례를 올림
if should_stop():
raise expand.AnalysisStopped
calls = _patch_live(monkeypatch, preview)
result = asyncio.run(live.post_drainage_live(pid, {"generation": 7, "points": []}))
assert result == live.SUPERSEDED and calls.build == 0
def test_newer_generation_after_preview_skips_split(monkeypatch):
pid = uuid4()
def preview(vertices, contours, streams, store, should_stop):
assert not should_stop()
live._LATEST_GENERATION[str(pid)] = 3
return SimpleNamespace(routing=object())
calls = _patch_live(monkeypatch, preview)
result = asyncio.run(live.post_drainage_live(pid, {"generation": 2}))
assert result == live.SUPERSEDED and calls.build == 0
def test_engine_stops_in_expansion_round():
contours = [
{
"geometry": {
"type": "LineString",
"coordinates": [
[150 + r * math.cos(t), -150 + r * math.sin(t)]
for t in np.linspace(0, 2 * math.pi, 121)
],
},
"properties": {"elevation": 300 - r},
}
for r in range(10, 200, 10)
]
spec = GridSpec(x_min=100.0, y_max=-100.0, cell_m=1.0, n_rows=100, n_cols=100)
domain = np.zeros((100, 100), bool)
domain[20:80, 20:80] = True
route = LineString([(110.0, -150.0), (190.0, -150.0)])
with pytest.raises(expand.AnalysisStopped):
expand.expand_by_red_boundary(
spec, domain, contours, route, [], None, should_stop=lambda: True
)
@pytest.mark.parametrize(
"recompute, stale, expected", [(True, True, 1), (True, False, 0), (False, True, 0)]
)
def test_confirm_recomputes_only_when_stale(monkeypatch, recompute, stale, expected):
import B04_PreProcess.B04_PreProcess_Router_Watershed as watershed
calls = []
async def is_stale(project_id):
return stale
async def region(project_id, refresh=False):
calls.append(refresh)
return {"status": "success"}
async def resolve(*args, **kwargs):
return basins.JSONResponse(status_code=404, content={})
monkeypatch.setattr(basins, "_analysis_stale", is_stale)
monkeypatch.setattr(watershed, "get_primary_region", region)
monkeypatch.setattr(basins, "_resolve", resolve)
asyncio.run(basins.put_pipe_points(uuid4(), {"points": []}, None, recompute))
assert len(calls) == expected and all(calls)