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
@@ -22,10 +22,14 @@
from __future__ import annotations
import hashlib
import json
import logging
import time
import math
import time
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import numpy as np
@@ -349,6 +353,99 @@ def build_contour_descent(
)
# ── 흐름 방향 저장본(347-1) ─────────────────────────────────────────────
#
# 방향장은 등고선 · 남긴 등고 단 · 격자(원점 · 셀 · 범위)만으로 정해진다. 원점이 좌표 정수 m 라
# 노선이 저장 범위 안이면 읽어서 잘라 쓴다. 파일 이름 = descent_<입력 지문>_<범위 지문>.npz ·
# 종류마다 최근 2벌만 남긴다. 계산 규칙이 바뀌면 `DESCENT_FILE_VERSION` 을 올린다.
DESCENT_FILE_VERSION = 1
_DESCENT_KEEP = 2
def descent_input_key(
contour_key: str,
contour_features: list[dict[str, Any]],
elevation_floor_m: float | None,
cell_m: float,
) -> str:
"""방향장 입력 지문 — 도엽 등고선 지문 + 남긴 등고 단 + 셀 + 계산 규칙 값.
남긴 단은 속성 표고만 본다 — 같은 도엽 · 같은 단 집합이면 남는 선도 같다(길이 거름은 결정적).
"""
levels = sorted(
{
elevation
for feature in contour_features
if feature.get("geometry")
and (elevation := _feature_elevation(feature.get("properties") or {})) is not None
and (elevation_floor_m is None or elevation >= elevation_floor_m)
}
)
text = json.dumps(
[
DESCENT_FILE_VERSION,
contour_key,
levels,
cell_m,
DRAINAGE_CONTOUR_MIN_LENGTH_M,
DESCENT_WINDOW_MARGIN_CELLS,
DESCENT_TILE_CELLS,
]
)
return hashlib.sha1(text.encode(), usedforsecurity=False).hexdigest()[:16]
def iter_saved_descents(directory: Path, input_key: str) -> Iterator[ContourDescent]:
"""같은 입력 지문으로 저장한 방향장 — 최근 것부터. 못 읽는 파일은 건너뜀."""
paths = sorted(
directory.glob(f"descent_{input_key}_*.npz"),
key=lambda path: path.stat().st_mtime,
reverse=True,
)
for path in paths:
try:
with np.load(path, allow_pickle=False) as data:
x_min, y_max, cell_m, n_rows, n_cols = data["spec"].tolist()
yield ContourDescent(
spec=GridSpec(
float(x_min), float(y_max), float(cell_m), int(n_rows), int(n_cols)
),
band_elevation=data["band_elevation"],
valid=data["valid"],
receiver=data["receiver"],
step_length=data["step_length"],
azimuth=data["azimuth"],
levels=data["levels"].tolist(),
)
except (OSError, KeyError, ValueError):
logger.warning("배수유역: 흐름 방향 저장본을 읽지 못해 건너뜁니다 (%s).", path)
def save_descent(directory: Path, input_key: str, descent: ContourDescent) -> None:
"""방향장을 저장하고 최근 2벌만 남긴다. 실패는 로그만(계산은 이어감)."""
spec = descent.spec
extent = f"{spec.x_min:.3f},{spec.y_max:.3f},{spec.n_rows},{spec.n_cols}"
extent_key = hashlib.sha1(extent.encode(), usedforsecurity=False).hexdigest()[:8]
path = directory / f"descent_{input_key}_{extent_key}.npz"
try:
directory.mkdir(parents=True, exist_ok=True)
np.savez(
path,
spec=np.array([spec.x_min, spec.y_max, spec.cell_m, spec.n_rows, spec.n_cols]),
band_elevation=descent.band_elevation,
valid=descent.valid,
receiver=descent.receiver,
step_length=descent.step_length,
azimuth=descent.azimuth,
levels=np.array(descent.levels, dtype=np.float64),
)
olds = sorted(directory.glob("descent_*.npz"), key=lambda item: item.stat().st_mtime)
for old in olds[:-_DESCENT_KEEP]:
old.unlink(missing_ok=True)
except OSError:
logger.warning("배수유역: 흐름 방향 저장본을 남기지 못했습니다 (%s).", path)
def _route_by_potential(
spec: GridSpec,
potential: np.ndarray,