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:
@@ -15,6 +15,7 @@ LAS 없는 설계(2026-08-30 사용자 확정)의 지형 원천이자, LAS가
|
||||
격자에서 뽑는 1m 등고선이 곧 2D 보간선이며, 메시(glb)는 그 격자의 표현일 뿐이다.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
@@ -24,6 +25,7 @@ from typing import Any
|
||||
import numpy as np
|
||||
from pyproj import Transformer
|
||||
|
||||
from B04_PreProcess.B04_PreProcess_Engine import file_stamp
|
||||
from B04_PreProcess.B04_PreProcess_Engine_ModelContext import (
|
||||
atomic_npz,
|
||||
clip_and_compact_mesh,
|
||||
@@ -36,6 +38,7 @@ from B04_PreProcess.B04_PreProcess_Engine_SheetMethods import (
|
||||
SHEET_METHOD_LABELS,
|
||||
)
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Grid import grid_spec_from_bounds
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from config.config_system import (
|
||||
SHEET_SURFACE_GRID_M,
|
||||
SHEET_SURFACE_MARGIN_M,
|
||||
@@ -54,6 +57,9 @@ _CONTOUR_FILE = "도엽_등고선.geojson"
|
||||
# 산출 모델 식별자 — surface_models.generation_params.source_filter 및 파일 stem에 쓴다.
|
||||
SHEET_SOURCE_FILTER = "sheet"
|
||||
|
||||
# 계산 판 번호 — 만드는 방식이 바뀌면 올림 · 열쇠가 어긋나 옛 저장본을 다시 셈.
|
||||
SHEET_SURFACE_ALGO = 1
|
||||
|
||||
|
||||
def _load_features_metric(
|
||||
processed_dir: Path, crs: str, filename: str = _CONTOUR_FILE
|
||||
@@ -378,6 +384,15 @@ def _write_method_model(
|
||||
vertices, faces = _preview_mesh(x_coords, y_coords, z_grid, valid_grid)
|
||||
write_glb(preview_path, vertices, faces, scene)
|
||||
_write_smoothed(models_dir, stem, x_coords, y_coords, z_grid, valid_grid, bounds, scene)
|
||||
return _method_model_entry(project_root, models_dir, method_key)
|
||||
|
||||
|
||||
def _method_model_entry(project_root: Path, models_dir: Path, method_key: str) -> dict[str, Any]:
|
||||
"""방식 하나의 등록용 dict — 저장본 그대로 쓸 때도 같은 모양."""
|
||||
source_filter = f"{SHEET_SOURCE_FILTER}_{method_key}"
|
||||
stem = f"dtm_{source_filter}"
|
||||
model_path = models_dir / f"{stem}.npz"
|
||||
preview_path = models_dir / f"{stem}_preview.glb"
|
||||
return {
|
||||
"model_type": "dtm",
|
||||
"source_filter": source_filter,
|
||||
@@ -391,6 +406,7 @@ def _write_method_model(
|
||||
"interpolation": method_key,
|
||||
"interpolation_label": SHEET_METHOD_LABELS.get(method_key, method_key),
|
||||
"margin_m": SHEET_SURFACE_MARGIN_M,
|
||||
"file_stamp": file_stamp(model_path),
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
@@ -403,6 +419,59 @@ def _write_method_model(
|
||||
}
|
||||
|
||||
|
||||
def _sheet_key(
|
||||
project_root: Path, processed_dir: Path, route_xy: np.ndarray, crs: str, method_key: str
|
||||
) -> dict[str, Any] | None:
|
||||
"""도엽 3D 면 열쇠 — 등고선 파일 · 노선 · 좌표계 · 격자 · 여유 · 스무딩 · 화면 상자 · 판."""
|
||||
contour = processed_dir / _CONTOUR_FILE
|
||||
try:
|
||||
stat = contour.stat()
|
||||
except OSError:
|
||||
return None
|
||||
scene = _scene_bounds(project_root, None) # type: ignore[arg-type] # 라이다 없으면 None
|
||||
key = {
|
||||
"algo": SHEET_SURFACE_ALGO,
|
||||
"method": method_key,
|
||||
"contour": f"{contour.name}:{stat.st_mtime_ns}:{stat.st_size}",
|
||||
"route": hashlib.sha1(np.round(route_xy, 3).tobytes()).hexdigest(),
|
||||
"crs": str(crs),
|
||||
"grid_m": SHEET_SURFACE_GRID_M,
|
||||
"margin_m": SHEET_SURFACE_MARGIN_M,
|
||||
"smoothing": [
|
||||
SURFACE_SMOOTHING_DTM_SIGMA_M,
|
||||
SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH,
|
||||
SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M,
|
||||
SURFACE_MAX_PREVIEW_VERTICES,
|
||||
],
|
||||
"scene_bounds": None if scene is None else np.round(scene, 3).tolist(),
|
||||
}
|
||||
return json.loads(json.dumps(key, default=float))
|
||||
|
||||
|
||||
def _key_path(models_dir: Path, method_key: str) -> Path:
|
||||
return models_dir / f"dtm_{SHEET_SOURCE_FILTER}_{method_key}.key.json"
|
||||
|
||||
|
||||
def _cached_method_model(
|
||||
project_root: Path, models_dir: Path, method_key: str, key: dict[str, Any] | None
|
||||
) -> dict[str, Any] | None:
|
||||
"""열쇠가 같고 결과 파일이 있으면 등록용 dict — 아니면 None(다시 셈)."""
|
||||
if key is None:
|
||||
return None
|
||||
entry = _method_model_entry(project_root, models_dir, method_key)
|
||||
if not all((project_root / item).is_file() for item in _entry_files(entry)):
|
||||
return None
|
||||
try:
|
||||
stored = json.loads(_key_path(models_dir, method_key).read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return entry if stored == key else None
|
||||
|
||||
|
||||
def _entry_files(entry: dict[str, Any]) -> list[str]:
|
||||
return [entry["model_file_path"], *(layer["file_path"] for layer in entry["layers"])]
|
||||
|
||||
|
||||
def build_sheet_surface_model(
|
||||
project_root: Path,
|
||||
processed_dir: Path,
|
||||
@@ -420,6 +489,16 @@ def build_sheet_surface_model(
|
||||
`route_xy`: (N, 2) 노선 정점 XY(사업지 CRS, m), `crs`: 그 좌표계(pyproj 입력 문자열).
|
||||
"""
|
||||
started = time.monotonic()
|
||||
selected = methods or list(SHEET_SURFACE_METHODS)
|
||||
# 열쇠가 같은 방식은 저장본 그대로 — 다시 셀 방식만 등고선을 읽고 굽는다.
|
||||
keys = {key: _sheet_key(project_root, processed_dir, route_xy, crs, key) for key in selected}
|
||||
cached = {
|
||||
key: _cached_method_model(project_root, models_dir, key, keys[key]) for key in selected
|
||||
}
|
||||
pending = [key for key in selected if cached[key] is None]
|
||||
if not pending:
|
||||
logger.info("도엽 서피스: 열쇠 같음 — 저장본 그대로 (%s)", ", ".join(selected))
|
||||
return [cached[key] for key in selected] # type: ignore[misc]
|
||||
features = _load_features_metric(processed_dir, crs)
|
||||
if not features:
|
||||
return []
|
||||
@@ -439,9 +518,8 @@ def build_sheet_surface_model(
|
||||
# 등고 간격(m) — 마루 연장 상한의 근거. 레벨이 하나뿐이면 5m(주곡선) 폴백.
|
||||
interval_m = float(np.diff(np.array(present)).min()) if len(present) > 1 else 5.0
|
||||
|
||||
selected = methods or list(SHEET_SURFACE_METHODS)
|
||||
models: list[dict[str, Any]] = []
|
||||
for method_key in selected:
|
||||
built: dict[str, dict[str, Any]] = {}
|
||||
for method_key in pending:
|
||||
builder = SHEET_METHOD_BUILDERS.get(method_key)
|
||||
if builder is None:
|
||||
logger.warning("도엽 서피스: 알 수 없는 보간 방식 %s — 건너뜁니다.", method_key)
|
||||
@@ -456,9 +534,13 @@ def build_sheet_surface_model(
|
||||
except Exception as exc: # noqa: BLE001 — 한 방식이 죽어도 나머지는 만든다
|
||||
logger.warning("도엽 서피스(%s) 생성 실패: %s", method_key, exc)
|
||||
continue
|
||||
# 쓰는 도중 끊기면 옛 열쇠가 새 파일 조각을 덮지 않게 먼저 지움.
|
||||
_key_path(models_dir, method_key).unlink(missing_ok=True)
|
||||
model = _write_method_model(project_root, models_dir, spec, surface, method_key)
|
||||
if model is not None:
|
||||
models.append(model)
|
||||
built[method_key] = model
|
||||
if keys[method_key] is not None:
|
||||
atomic_write_json(_key_path(models_dir, method_key), keys[method_key])
|
||||
logger.info("도엽 서피스(%s) 완료 (%.1fs)", method_key, time.monotonic() - step_started)
|
||||
|
||||
logger.info(
|
||||
@@ -466,10 +548,10 @@ def build_sheet_surface_model(
|
||||
spec.n_rows,
|
||||
spec.n_cols,
|
||||
len(present),
|
||||
len(models),
|
||||
len(built),
|
||||
time.monotonic() - started,
|
||||
)
|
||||
return models
|
||||
return [cached[key] or built[key] for key in selected if cached[key] or key in built]
|
||||
|
||||
|
||||
def build_sheet_surface_from_route(
|
||||
|
||||
Reference in New Issue
Block a user