perf(영구저장): 343-2 도엽 3D 면 열쇠 · 지표면 모델 id 유지 · 비용면 메모리 기억 · 추천 노선 열쇠 · 횡단 상세 ETag

- 도엽 3D 면 — 결과 옆 dtm_sheet_{방식}.key.json(등고선 이름:mtime:크기 · 노선 · CRS · 격자 · 여유 · 스무딩 · 화면 상자 · ALGO) 같으면 건너뜀
- 지표면 모델 — (방식 · 필터 · 파일) + 파일 지문 같으면 행 id 살려 고쳐 씀 · 이번 결과에 없는 옛 행만 지움
- 비용면 · 계류 메모리 기억(비용면 서명 · 계류 파일 지문 · 좌표계) · 추천 열쇠 = 서명(배열 통째 해시 없앰)
- 추천 저장본 종류(갈래 · 지형)당 최근 2벌 · 꺼내면 최근으로
- 횡단 상세 GET — 측점 파일 안 지움(valid_cross_names) · 본문 해시 ETag 304
- 시험 test_343_2_persist_keys 13건

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
2026-10-16 11:10:13 +09:00
co-authored by Claude Opus 5.5
parent cf5de56418
commit 1fcdf5aa4c
9 changed files with 706 additions and 92 deletions
+56 -12
View File
@@ -109,12 +109,19 @@ MODE_WEIGHTS = {
# 후보 묶음은 갈래와 무관 — 같은 입력이면 다시 짜지 않음(최근 몇 벌만 · 프로세스 메모리)
POOL_CACHE_SIZE = 4
_POOLS: dict[bytes, Pool] = {}
# 비용면 · 계류 기억(요청 사이) — 열쇠 = 프로젝트 · 비용면 서명 · 계류 파일 지문 · 좌표계
TERRAIN_CACHE_SIZE = 4
_TERRAINS: dict[str, tuple[Terrain, list[list[tuple[float, float]]]]] = {}
def load_terrain_and_streams(
project_root: Path, selection: dict[str, Any]
) -> tuple[Terrain, list[list[tuple[float, float]]]]:
"""지반 격자(탐색 비용면 캐시 그대로)와 이격을 따질 계류(사업지 좌표계)를 읽는다."""
"""지반 격자(탐색 비용면 캐시 그대로)와 이격을 따질 계류(사업지 좌표계)를 읽는다.
같은 입력(비용면 서명 · 계류 파일 지문 · 좌표계)이면 메모리 기억을 그대로 돌려준다 —
점 놓을 때마다 npz · geojson 을 다시 읽지 않는다. 돌려준 배열은 고치지 않는다.
"""
from pyproj import Transformer
from B04_PreProcess.B04_PreProcess_Router_Watershed import (
@@ -122,20 +129,49 @@ def load_terrain_and_streams(
_load_features,
_reproject_features,
)
from B05_Profile.B05_Profile_Engine_Solver import _MODELS_SUBDIR, _load_or_build_cost_surface
from B05_Profile.B05_Profile_Engine_Solver import (
_MODELS_SUBDIR,
_cost_surface_signature,
_load_or_build_cost_surface,
)
from common_util.common_util_crs import resolve_project_crs
x, y, z, valid, *_ = _load_or_build_cost_surface(
project_root,
project_root / _MODELS_SUBDIR,
str(selection.get("source_filter")),
str(selection.get("method") or "dtm"),
bool(selection.get("smooth")),
filter_key = str(selection.get("source_filter"))
method = str(selection.get("method") or "dtm")
smooth = bool(selection.get("smooth"))
models_dir = project_root / _MODELS_SUBDIR
stream_path = project_root / "B04_PreProcess" / "processed" / STREAM_FILE
crs = resolve_project_crs(project_root)
try:
stat = stream_path.stat()
stream_stamp = f"{stat.st_mtime_ns}:{stat.st_size}"
except OSError:
stream_stamp = "-"
signature = "|".join(
(
filter_key,
_cost_surface_signature(models_dir, filter_key, method, smooth),
stream_stamp,
str(crs),
)
)
to_metric = Transformer.from_crs("EPSG:4326", resolve_project_crs(project_root), always_xy=True)
features = _load_features(project_root / "B04_PreProcess" / "processed", STREAM_FILE)
memo_key = f"{project_root}|{signature}"
if memo_key in _TERRAINS:
return _TERRAINS[memo_key]
x, y, z, valid, *_ = _load_or_build_cost_surface(
project_root, models_dir, filter_key, method, smooth
)
to_metric = Transformer.from_crs("EPSG:4326", crs, always_xy=True)
features = _load_features(stream_path.parent, STREAM_FILE)
streams = stream_lines(_reproject_features(features, to_metric))
return Terrain(np.asarray(x), np.asarray(y), np.asarray(z), np.asarray(valid)), streams
terrain = Terrain(
np.asarray(x), np.asarray(y), np.asarray(z), np.asarray(valid), signature=signature
)
while len(_TERRAINS) >= TERRAIN_CACHE_SIZE:
_TERRAINS.pop(next(iter(_TERRAINS)))
_TERRAINS[memo_key] = (terrain, streams)
return terrain, streams
def _choice_path(project_root: Path) -> Path:
@@ -378,8 +414,16 @@ def _pool_key(
streams: list[list[tuple[float, float]]],
*numbers: float,
) -> bytes:
"""후보 묶음을 가를 입력 지문 — 지형 · 예상노선 · 계류 · 제약 값."""
"""후보 묶음을 가를 입력 지문 — 지형 · 예상노선 · 계류 · 제약 값.
지형에 입력 지문(`signature` — 비용면 서명 + 계류 파일)이 있으면 배열 통째 해시 대신 씀.
"""
digest = hashlib.blake2b(digest_size=16)
if terrain.signature:
digest.update(terrain.signature.encode())
digest.update(np.ascontiguousarray(np.asarray(expected)).tobytes())
digest.update(repr(numbers).encode())
return digest.digest()
for array in (terrain.x, terrain.y, terrain.z, terrain.valid, np.asarray(expected)):
digest.update(np.ascontiguousarray(array).tobytes())
digest.update(repr((streams, numbers)).encode())