perf(배수유역): 347-1 흐름 방향 · 도엽 좌표 저장 — 격자 원점 좌표 정수 m · 노선이 범위 안이면 읽기만

- 격자 원점 = 좌표 정수 m(GRID_ORIGIN_XY · 옛 「도로 시작점」 지시를 바꿈 · 사용자 확정)
- 흐름 방향 저장본 drainage/descent_<입력 지문>_<범위>.npz — 입력 지문 = 도엽 등고선 지문 + 남긴 등고 단 + 셀 + 계산 규칙 · 덮으면 읽어 잘라 씀 · 최근 2벌
- 도엽(하천 · 등고선) 사업지 좌표 저장본 drainage/00_sheet_metric.pkl — 원본 자국 · 좌표계가 같으면 재투영 안 함
- 검증_sub3 실측: 저장본 읽기 = 지은 것과 칸마다 같음(적색 · 해석 영역 · 화살표 · 귀속 차이 0) · 108 → 10.8 s(붐빔 · 340 기준 약 6 s) · 원점 바뀜으로 적색 460,035 → 459,792칸(−0.05 %) · 관 자리 같음
- 시험 test_347_1_descent_store · 실측 bench 저장본 갈래

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-16 11:21:13 +09:00
co-authored by Claude Opus 5.5
parent fba1eb8a5d
commit 0e0c1be1c0
8 changed files with 334 additions and 31 deletions
+11 -4
View File
@@ -3,9 +3,10 @@
./venv/Scripts/python.exe resources/tester/bench_342_watershed.py base # 기준 저장
./venv/Scripts/python.exe resources/tester/bench_342_watershed.py new base # 새로 돌려 대조
./venv/Scripts/python.exe resources/tester/bench_342_watershed.py s1 base store # 흐름 방향 저장본
pytest 대상 아님(DB · 프로젝트 파일이 있어야 돎). 저장 안 함 — 프로젝트 파일은 읽기만
(흐름 방향 저장본 drainage/descent_*.npz 는 347-1 부터 씀).
pytest 대상 아님(DB · 프로젝트 파일이 있어야 돎). 분석 결과는 저장 안 함 — 도엽 좌표 저장본
(drainage/00_sheet_metric.pkl)만 프로젝트에 남고 · 흐름 방향 저장본은 tmp/342/descent 에 둠.
"""
import asyncio
@@ -40,7 +41,8 @@ async def _prepare():
def main() -> None:
name = sys.argv[1]
base = sys.argv[2] if len(sys.argv) > 2 else None
base = sys.argv[2] if len(sys.argv) > 2 and sys.argv[2] != "-" else None
store = len(sys.argv) > 3 and sys.argv[3] == "store"
OUT_DIR.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
level=logging.INFO,
@@ -51,7 +53,12 @@ def main() -> None:
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import preview_stages
started = time.perf_counter()
pv = preview_stages(prepared["vertices"], prepared["contours"], prepared["streams"])
pv = preview_stages(
prepared["vertices"],
prepared["contours"],
prepared["streams"],
(OUT_DIR / "descent", prepared["contour_key"]) if store else None,
)
preview_s = time.perf_counter() - started
spec = pv.spec
red = pv.flow.reaches_road & pv.flow.analyzed
@@ -0,0 +1,128 @@
"""347-1 흐름 방향 · 도엽 좌표 저장 — 저장본을 읽어도 결과 같음 · 두 번째는 짓지 않음.
- 격자 원점 = 좌표 정수 m(셀 모서리 0,0) — 노선이 바뀌어도 같은 자리 셀은 같은 자리.
- 방향장 저장본: 입력 지문(도엽 지문 · 남긴 등고 단 · 셀) 같고 범위가 덮으면 읽기만 · 최근 2벌.
- 도엽 좌표 저장본: 원본 자국 · 좌표계가 같으면 다시 안 바꿈.
실측(검증_sub3 · 차이 0칸 · 108 → 10.8 s 붐빔 · 340 기준 약 6 s)은 bench_342_watershed.py.
"""
import math
import numpy as np
from shapely.geometry import LineString
from B04_PreProcess import B04_PreProcess_Engine_Watershed_Expand as expand
from B04_PreProcess import B04_PreProcess_Router_Watershed as router
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Descent import (
descent_input_key,
iter_saved_descents,
)
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Grid import (
GRID_ORIGIN_XY,
GridSpec,
grid_spec_from_bounds,
)
def _contours():
features = []
for radius in range(10, 200, 10):
coords = [
[150.0 + radius * math.cos(t), -150.0 + radius * math.sin(t)]
for t in np.linspace(0, 2 * math.pi, 241)
]
features.append(
{
"geometry": {"type": "LineString", "coordinates": coords},
"properties": {"elevation": 300.0 - radius},
}
)
return features
def test_grid_origin_is_integer_metre():
assert GRID_ORIGIN_XY == (0.0, 0.0)
spec = grid_spec_from_bounds(10.37, -80.6, 90.2, -3.4, 1.0, anchor_xy=GRID_ORIGIN_XY)
assert spec.x_min == 10.0 and spec.y_max == -3.0
def test_input_key_follows_contours_and_floor():
contours = _contours()
base = descent_input_key("도엽-1", contours, None, 1.0)
assert base == descent_input_key("도엽-1", contours, None, 1.0)
assert base != descent_input_key("도엽-2", contours, None, 1.0) # 도엽 · 좌표계 바뀜
assert base != descent_input_key("도엽-1", contours, 200.0, 1.0) # 남긴 등고 단 바뀜
assert base == descent_input_key("도엽-1", contours, 100.5, 1.0) # 단 집합 그대로(전부 남음)
def _run(store, monkeypatch, counter):
real = expand.build_contour_descent
def counting(*args, **kwargs):
counter.append(1)
return real(*args, **kwargs)
monkeypatch.setattr(expand, "build_contour_descent", counting)
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)])
result = expand.expand_by_red_boundary(
spec, domain, _contours(), route, [], None, descent_store=store
)
return result.analysis
def test_store_gives_same_result_and_reads_second_time(tmp_path, monkeypatch):
monkeypatch.setattr(expand, "DESCENT_SPARE_M", 40.0)
built: list[int] = []
plain = _run(None, monkeypatch, built)
store = (tmp_path, "도엽-1")
first = _run(store, monkeypatch, built)
count_after_first = len(built)
second = _run(store, monkeypatch, built)
assert len(built) == count_after_first # 두 번째는 짓지 않음
for analysis in (first, second):
assert analysis.spec == plain.spec
assert np.array_equal(analysis.flow.reaches_road, plain.flow.reaches_road)
assert np.array_equal(analysis.descent.receiver, plain.descent.receiver)
saved = list(tmp_path.glob("descent_*.npz"))
assert 1 <= len(saved) <= 2
key = descent_input_key("도엽-1", _contours(), None, 1.0)
assert next(iter_saved_descents(tmp_path, key)).spec.n_rows >= plain.spec.n_rows
def test_store_keeps_two_newest(tmp_path, monkeypatch):
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Descent import save_descent
monkeypatch.setattr(expand, "DESCENT_SPARE_M", 0.0)
analysis = _run(None, monkeypatch, [])
for index in range(3):
moved = GridSpec(100.0 + index, -100.0, 1.0, analysis.spec.n_rows, analysis.spec.n_cols)
descent = analysis.descent
save_descent(tmp_path, "key", type(descent)(**{**descent.__dict__, "spec": moved}))
assert len(list(tmp_path.glob("descent_*.npz"))) == 2
def test_metric_sheets_cached_until_source_changes(tmp_path, monkeypatch):
sheets = tmp_path / "sheets"
sheets.mkdir()
feature = {
"type": "Feature",
"properties": {"elevation": 10},
"geometry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]},
}
import json
for name in (router.STREAM_FILE, router.CONTOUR_FILE):
(sheets / name).write_text(json.dumps({"features": [feature]}), "utf-8")
calls = []
real = router._reproject_features
monkeypatch.setattr(
router, "_reproject_features", lambda f, t: calls.append(1) or real(f, None)
)
first = router._metric_sheets(sheets, tmp_path / "drainage", "EPSG:5186", None)
again = router._metric_sheets(sheets, tmp_path / "drainage", "EPSG:5186", None)
assert len(calls) == 2 and first == again # 두 번째는 저장본
router._metric_sheets(sheets, tmp_path / "drainage", "EPSG:5179", None) # 좌표계 바뀜
assert len(calls) == 4