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
@@ -9,10 +9,12 @@
③ 넓힌 영역으로 흐름 방향·색을 다시 분석한다
④ **새로 추가된 셀에 적색이 하나도 없으면 종료** (2026-07-31 사용자 지시)
격자 bbox에 닿으면 격자 자체도 셀 정수배로 넓힌다 — 도로 시작점 기준 격자점은 유지된다.
격자 bbox에 닿으면 격자 자체도 셀 정수배로 넓힌다 — 좌표 정수 m 기준 격자점은 유지된다.
하강 방향장은 처음에 **넉넉한 큰 격자**로 한 번 짓고, 회차마다 그 격자에서 잘라 쓴다
(`DESCENT_SPARE_M` · 342-3). 격자가 커질 때마다 다시 짓던 것(8회 · 30 s)을 한 번으로 줄인다.
저장소(`descent_store` = 폴더 · 도엽 등고선 지문)를 주면 지은 것을 남기고, 덮는 저장본이 있으면
읽기만 한다(347-1).
"""
from __future__ import annotations
@@ -20,6 +22,7 @@ from __future__ import annotations
import logging
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import numpy as np
@@ -28,6 +31,9 @@ from shapely.geometry import LineString
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Descent import (
ContourDescent,
build_contour_descent,
descent_input_key,
iter_saved_descents,
save_descent,
)
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Flow import (
FlowClassification,
@@ -135,6 +141,7 @@ def expand_by_red_boundary(
elevation_floor_m: float | None = None,
band_m: float = DRAINAGE_RED_EXPAND_BAND_M,
max_rounds: int = DRAINAGE_RED_EXPAND_MAX_ROUNDS,
descent_store: tuple[Path, str] | None = None,
) -> RedExpansion | None:
"""최외곽 적색 셀 주변으로 넓히며, 새로 추가한 셀에 적색이 없을 때까지 반복한다."""
_expand_started = time.perf_counter()
@@ -144,9 +151,33 @@ def expand_by_red_boundary(
spec, domain = _pad_spec(spec, domain, band_cells * 2)
started_cells = int(domain.sum())
spare_cells = max(0, int(round(DESCENT_SPARE_M / spec.cell_m)))
source = build_contour_descent(
_pad_spec(spec, domain, spare_cells)[0], contour_features, None, elevation_floor_m
)
store = None
if descent_store is not None:
folder, contour_key = descent_store
store = (
folder,
descent_input_key(contour_key, contour_features, elevation_floor_m, spec.cell_m),
)
def source_for(grid: GridSpec, mask: np.ndarray) -> ContourDescent:
"""`grid` 를 덮는 큰 방향장 — 저장본이 덮으면 읽기만 · 아니면 둘레 여유째 짓고 남김."""
if store is not None:
for saved in iter_saved_descents(*store):
if crop_descent(saved, grid) is not None:
logger.info(
"배수유역: 흐름 방향 저장본을 읽어 씁니다(%d×%d).",
saved.spec.n_rows,
saved.spec.n_cols,
)
return saved
built = build_contour_descent(
_pad_spec(grid, mask, spare_cells)[0], contour_features, None, elevation_floor_m
)
if store is not None:
save_descent(*store, built)
return built
source = source_for(spec, domain)
analysis = analyze_domain(
spec, domain, contour_features, route_line, upstream_streams, elevation_floor_m, source
)
@@ -181,12 +212,7 @@ def expand_by_red_boundary(
break
if crop_descent(source, grown_spec) is None:
# 넉넉히 둔 방향장 밖으로 넓어짐 — 넓힌 격자 둘레로 다시 짓는다.
source = build_contour_descent(
_pad_spec(grown_spec, grown_domain, spare_cells)[0],
contour_features,
None,
elevation_floor_m,
)
source = source_for(grown_spec, grown_domain)
logger.info(
"배수유역: %d회차 확장 — 최외곽 적색 %d셀 주변 %.0fm, 셀 %d개 추가",
@@ -329,7 +355,7 @@ def _grow_for_rim(
) -> tuple[GridSpec, np.ndarray, np.ndarray]:
"""적색 최외곽이 격자 bbox에 닿았으면 그 방향으로 격자를 넓히고 마스크를 옮겨 담는다.
격자는 셀 정수배로만 넓히므로 도로 시작점 기준 격자점이 그대로 유지된다.
격자는 셀 정수배로만 넓히므로 좌표 정수 m 기준 격자점이 그대로 유지된다.
"""
north = band_cells if rim[0, :].any() else 0
south = band_cells if rim[-1, :].any() else 0