Files
Aislo/resources/tester/test_614_terrain_reproject.py
T
eomsangdonandClaude Opus 5.5 f97c8975a3 feat(공용 지형): 띠 밖 좌표계 = 띠 지형 쌍선형 옮겨 담기 (PLAN 614-1 · 요구 494)
- ensure_sheet_terrain: 도엽 띠(5185~5187) 아닌 좌표계면 띠 저장본(없으면 띠로 먼저 셈)을 그 좌표계 2 m 격자로 쌍선형 옮겨 담아 저장 · 열쇠 via = 띠
- 이미 직접 센 저장본 그대로 · REPROJECT_FROM_BAND 한 줄로 끔 · 가장자리 = 창고에 있는 이웃 띠만 · 없으면 직선 연장
- 문턱(브레인) = 참값(등고선 위 점) 대비 오차 · 옮김이 더하는 95 % ≤ 0.10 m · 최대 ≤ 직접 최대
- 시험 test_614 새로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:39:15 +09:00

126 lines
5.8 KiB
Python

"""띠 밖 좌표계 = 띠 지형 옮겨 담기(PLAN 614-1) — 띠로 먼저 셈 · 옮긴 것도 그 좌표계 열쇠로 저장 ·
둘째 읽기 셈 없음 · 참값(등고선 위 점) 대비 옮김 오차 ≤ 직접 + 0.10 m ·
이미 직접 센 저장본 그대로 · 끄면 직접 셈 ·
빈 띠 = 빈 도엽 · 가장자리 직선 연장.
./venv/Scripts/python.exe -m pytest resources/tester/test_614_terrain_reproject.py -q
"""
import json
import numpy as np
import pytest
from pyproj import Transformer
from B04_PreProcess.B04_PreProcess_Engine_MapSheet import sheet5k_to_bounds
from common_util import common_util_terrain_store as store
from common_util import common_util_terrain_surface as surf
BAND, OTHER = 5187, 5186
SHEET = "36901074"
CELL = 10.0
@pytest.fixture
def fake(tmp_path, monkeypatch):
"""띠(5187) 좌표의 굽은 비탈(남서 먼 곳 중심 동심 호 · 간격 점점 넓음) — 요청 좌표계로 옮겨
상자 안 선분만 돌려줌."""
lon0, lat0, _, _ = sheet5k_to_bounds(SHEET)
to_band = Transformer.from_crs("EPSG:4326", f"EPSG:{BAND}", always_xy=True)
cx, cy = to_band.transform(lon0 - 0.04, lat0 - 0.04)
arc = np.linspace(0.0, np.pi / 2, 401)[:, None]
lines = []
for k in range(250):
r = 3000.0 + 30.0 * k + 0.1 * k * k
lines.append((np.hstack([cx + r * np.cos(arc), cy + r * np.sin(arc)]), 500.0 + 5.0 * k))
a, b, z = surf.segments_from_lines(lines)
gpkg = tmp_path / "national.gpkg"
gpkg.write_bytes(b"v1")
calls = {"build": 0}
real_build = store.build_contour_surface
def segments(epsg, box):
tr = Transformer.from_crs(f"EPSG:{BAND}", store._crs(epsg), always_xy=True)
pa, pb = np.column_stack(tr.transform(*a.T)), np.column_stack(tr.transform(*b.T))
hit = (np.maximum(pa[:, 0], pb[:, 0]) >= box[0]) & (
np.minimum(pa[:, 0], pb[:, 0]) <= box[2]
)
hit &= (np.maximum(pa[:, 1], pb[:, 1]) >= box[1]) & (
np.minimum(pa[:, 1], pb[:, 1]) <= box[3]
)
return pa[hit], pb[hit], z[hit]
def counted(*args):
calls["build"] += 1
return real_build(*args)
monkeypatch.setattr(store, "TERRAIN_ROOT", tmp_path / "terrain")
monkeypatch.setattr(store, "NATIONAL_GPKG", gpkg)
monkeypatch.setattr(store, "_gpkg_segments", segments)
monkeypatch.setattr(store, "build_contour_surface", counted)
monkeypatch.setattr(store, "REPROJECT_FROM_BAND", True)
return calls
def _key(epsg):
path = store._names(SHEET, epsg, CELL)[1]
return json.loads(path.read_text(encoding="utf-8"))
def test_other_crs_reprojects_band_and_stores(fake):
assert store._via(SHEET, OTHER) == BAND and store._via(SHEET, BAND) is None
got = store.load_terrain(OTHER, sheets=[SHEET], cell_m=CELL)
assert got["computed"] == [SHEET] and not got["failed"]
assert fake["build"] == 1 # 띠 한 번만 셈 · 옮기기는 셈 아님
assert _key(BAND).get("via") is None and _key(OTHER)["via"] == BAND
assert _key(OTHER)["source"] == "reproject"
again = store.load_terrain(OTHER, sheets=[SHEET], cell_m=CELL)
assert again["computed"] == [] and fake["build"] == 1
assert np.array_equal(np.nan_to_num(again["z"], nan=-1), np.nan_to_num(got["z"], nan=-1))
# 직접 셈(끔)과 견줌 — 같은 칸 거의 다 · 높이 차 작음
store.REPROJECT_FROM_BAND = False
assert store._stored_ok(SHEET, OTHER, CELL) is None # 끄면 옮긴 저장본 버림
direct = store.load_terrain(OTHER, sheets=[SHEET], cell_m=CELL)
assert direct["computed"] == [SHEET] and fake["build"] == 2
assert _key(OTHER).get("via") is None
assert direct["z"].shape == got["z"].shape
both = direct["valid_mask"] & got["valid_mask"]
assert both.sum() > 0.98 * direct["valid_mask"].sum()
diff = np.abs(direct["z"][both] - got["z"][both])
assert float(np.percentile(diff, 95)) < 0.5 and float(diff.max()) < 2.0 # 10 m 칸 견줌
# 문턱(브레인) = 참값(등고선 위 점) 대비 오차 — 옮김이 더하는 95 % ≤ 0.10 m · 최대 ≤ 직접 최대
gx, gy = direct["x"], direct["y"]
a, b, z = store._gpkg_segments(OTHER, (gx[2], gy[2], gx[-3], gy[-3]))
px, py = (a[:, 0] + b[:, 0]) / 2, (a[:, 1] + b[:, 1]) / 2
inside = (px > gx[2]) & (px < gx[-3]) & (py > gy[2]) & (py < gy[-3])
err_direct = np.abs(store._bilinear(direct, px[inside], py[inside]) - z[inside])
err_moved = np.abs(store._bilinear(got, px[inside], py[inside]) - z[inside])
ok = np.isfinite(err_direct) & np.isfinite(err_moved)
assert ok.sum() > 1000
added = np.percentile(err_moved[ok], 95) - np.percentile(err_direct[ok], 95)
assert added <= 0.10 and err_moved[ok].max() <= err_direct[ok].max()
# 다시 켜도 직접 센 저장본은 그대로 씀
store.REPROJECT_FROM_BAND = True
assert store._stored_ok(SHEET, OTHER, CELL) is not None
store.load_terrain(OTHER, sheets=[SHEET], cell_m=CELL)
assert fake["build"] == 2
def test_empty_band_is_empty(fake, monkeypatch):
monkeypatch.setattr(store, "_gpkg_segments", lambda epsg, box: store._NO_SEGMENTS)
monkeypatch.setattr(store, "_fetch_zips", lambda sheet_no, report: ({}, None))
assert store.ensure_sheet_terrain(SHEET, OTHER, CELL) is None
assert _key(BAND)["empty"] and _key(OTHER)["empty"] and _key(OTHER)["via"] == BAND
assert store.ensure_sheet_terrain(SHEET, OTHER, CELL) is None
def test_extend_edges_and_bilinear_exact_on_plane():
y, x = np.mgrid[0:12, 0:15].astype(float)
plane = 3.0 * x - 2.0 * y + 7.0
z = np.full_like(plane, np.nan)
z[1:-1, 1:-1] = plane[1:-1, 1:-1]
assert np.allclose(store._extend_edges(z), plane)
grid = {"z": z, "x": np.arange(15) + 0.5, "y": np.arange(12) + 0.5, "cell_m": 1.0}
px, py = np.array([0.5, 7.3, 14.4]), np.array([0.6, 5.9, 11.5])
assert np.allclose(store._bilinear(grid, px, py), 3.0 * (px - 0.5) - 2.0 * (py - 0.5) + 7.0)