- 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
170 lines
6.4 KiB
Python
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)
|