feat(641-1): implicit 굳힌 격자 — 확정 지표면 B05 · B06 화면 일치

- implicit_{필터}_grid.npz = 같은 필터 dtm 축 · 유효 칸 · 유효 칸만 RBF · 밖 = dtm 값 · implicit · dtm 보다 옛것이면 다시 셈(파생 저장본)
- 서버 표본기 implicit = 굳힌 격자 DtmGridSampler(매번 RBF 다시 짓기 없앰) · Node 측점이 source 로 같은 격자
- B05 브라우저 격자 통로 · B06 서버 재계산 격자 = implicit 확정이면 같은 굳힌 격자(새 짝 없음 · 기존 heightAt 짝)
- 짓기 끝(build_implicit)에서 한 번 굳힘 · 못 지으면 처음 읽을 때

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 21:21:57 +09:00
co-authored by Claude Opus 5.5
parent 887ca4a035
commit f912efd36a
5 changed files with 91 additions and 33 deletions
+51 -15
View File
@@ -177,6 +177,55 @@ def dtm_grid_file(npz_path: Path | str) -> Path:
return target
def implicit_rbf(path: Path | str) -> Callable[[np.ndarray], np.ndarray]:
"""`implicit_{필터}.npz`(중심점 · 높이 · smoothing) → RBF 표고 함수((N, 2) → (N,))."""
from scipy.interpolate import RBFInterpolator
with np.load(Path(path), allow_pickle=False) as data:
centers = np.asarray(data["centers_xy"], dtype=np.float64)
center_z = np.asarray(data["center_z"], dtype=np.float64)
smoothing = float(data["smoothing"][0]) if "smoothing" in data else 0.0
return RBFInterpolator(
centers,
center_z,
neighbors=min(64, len(centers)),
smoothing=smoothing,
kernel="thin_plate_spline",
)
def implicit_grid_npz(models_dir: Path | str, source_filter: str) -> Path:
"""implicit 를 1 m 격자로 굳힌 파생 저장본 `implicit_{필터}_grid.npz`(641-1 · 요구 558-3).
축 · 유효 칸 = 같은 필터 `dtm_{필터}.npz` 그대로 · 유효 칸만 RBF · 밖은 dtm 값(NaN 없음).
없거나 implicit · dtm 보다 옛것이면 다시 셈 — 서버 표본 · 브라우저 · Node 측점 · B06 이 같은 값.
"""
check_surface_keys(source_filter, "implicit")
models_dir = Path(models_dir)
source = models_dir / f"implicit_{source_filter}.npz"
dtm = models_dir / f"dtm_{source_filter}.npz"
target = models_dir / f"implicit_{source_filter}_grid.npz"
newest = max(source.stat().st_mtime_ns, dtm.stat().st_mtime_ns)
if target.is_file() and target.stat().st_mtime_ns >= newest:
return target
with np.load(dtm, allow_pickle=False) as data:
x, y = data["x"], data["y"]
z = np.array(data["z"])
valid = np.asarray(data["valid_mask"], dtype=bool)
resolution = data["resolution"] if "resolution" in data else np.array([1.0], np.float32)
rbf = implicit_rbf(source)
rows, cols = np.nonzero(valid)
query = np.column_stack([x[cols], y[rows]]).astype(np.float64)
for start in range(0, len(query), 50_000):
end = start + 50_000
z[rows[start:end], cols[start:end]] = rbf(query[start:end])
temporary = target.with_name(f"{target.stem}.{os.getpid()}.tmp")
with open(temporary, "wb") as handle:
np.savez(handle, x=x, y=y, z=z, valid_mask=valid, resolution=resolution)
os.replace(temporary, target)
return target
@dataclass
class CallableSurfaceSampler:
"""테스트와 어댑터에 사용할 함수 기반 sampler."""
@@ -255,21 +304,8 @@ def build_surface_sampler(
return InterpolatedSurfaceSampler(lambda xy: spline.ev(xy[:, 1], xy[:, 0]), footprint)
if method == "implicit":
from scipy.interpolate import RBFInterpolator
path = models_dir / f"implicit_{source_filter}.npz"
with np.load(path, allow_pickle=False) as data:
centers = np.asarray(data["centers_xy"], dtype=np.float64)
center_z = np.asarray(data["center_z"], dtype=np.float64)
smoothing = float(data["smoothing"][0]) if "smoothing" in data else 0.0
interpolator = RBFInterpolator(
centers,
center_z,
neighbors=min(64, len(centers)),
smoothing=smoothing,
kernel="thin_plate_spline",
)
return InterpolatedSurfaceSampler(lambda xy: interpolator(xy), footprint)
# 굳힌 격자(641-1) — 브라우저 · Node 측점 · B06 이 같은 파일을 읽음(`source` 가 붙음)
return DtmGridSampler.open_cached(implicit_grid_npz(models_dir, source_filter))
if method == "meshfree":
from scipy.interpolate import LinearNDInterpolator