Files
Aislo/resources/tester/test_b04_contour_raster.py
T
eomsangdonandClaude Opus 5 0ef32b5279 chore(tester): 시험을 resources/tester/ 로 옮김 — 창끼리 건너가게
⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt`
가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는
시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음.

- `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침**
- `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시)
- 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q`
  옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0

⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 17:12:30 +09:00

196 lines
7.7 KiB
Python

"""B04 삼각망 래스터화 — 위치탐색 없이 격자 표고를 만드는 함수의 정확도 검증.
기존 경로는 scipy `griddata`가 질의점마다 삼각형 98만개 망을 탐색해 등고선 1회에
2~5분이 걸렸다(PLAN 2026-08-17). 삼각형을 격자에 직접 래스터화하면 탐색이 사라진다.
정밀도 유지가 조건이므로 무게중심 선형보간의 해석해 일치를 먼저 못박는다.
"""
import numpy as np
import pytest
from B04_PreProcess.B04_PreProcess_Engine_Contour import rasterize_triangle_mesh
def _axes(x_min, x_max, y_min, y_max, step=1.0):
cols = int(round((x_max - x_min) / step)) + 1
rows = int(round((y_max - y_min) / step)) + 1
return (
np.linspace(x_min, x_max, cols, dtype=np.float64),
np.linspace(y_min, y_max, rows, dtype=np.float64),
)
def _plane(x, y):
"""검증용 기준 평면 — 선형보간이 정확하면 오차 0이어야 한다."""
return 2.0 * x + 3.0 * y + 5.0
def test_single_triangle_matches_plane_solution():
"""평면 위 삼각형 하나 — 내부 격자점이 해석해와 일치."""
verts = np.array(
[
[0.0, 0.0, _plane(0.0, 0.0)],
[10.0, 0.0, _plane(10.0, 0.0)],
[0.0, 10.0, _plane(0.0, 10.0)],
]
)
tris = np.array([[0, 1, 2]])
x_coords, y_coords = _axes(0.0, 10.0, 0.0, 10.0)
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
xx, yy = np.meshgrid(x_coords, y_coords)
filled = np.isfinite(z_grid)
assert filled.any()
assert np.allclose(z_grid[filled], _plane(xx[filled], yy[filled]), atol=1e-9)
def test_cells_outside_mesh_stay_nan():
"""삼각망 밖은 NaN — footprint 마스크가 기존과 같이 동작해야 한다."""
verts = np.array([[0.0, 0.0, 1.0], [2.0, 0.0, 1.0], [0.0, 2.0, 1.0]])
tris = np.array([[0, 1, 2]])
x_coords, y_coords = _axes(0.0, 10.0, 0.0, 10.0)
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
# 빗변 바깥쪽 (9, 9)는 삼각형 밖
assert np.isnan(z_grid[9, 9])
# 꼭짓점 (0, 0)은 삼각형 안
assert z_grid[0, 0] == pytest.approx(1.0)
def test_two_triangles_fill_full_square():
"""사각형을 이루는 두 삼각형 — 격자 전체가 빈칸 없이 채워진다."""
corners = [(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)]
verts = np.array([[x, y, _plane(x, y)] for x, y in corners])
tris = np.array([[0, 1, 2], [0, 2, 3]])
x_coords, y_coords = _axes(0.0, 4.0, 0.0, 4.0)
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
assert np.isfinite(z_grid).all()
xx, yy = np.meshgrid(x_coords, y_coords)
assert np.allclose(z_grid, _plane(xx, yy), atol=1e-9)
def test_shared_edge_is_consistent():
"""두 삼각형이 공유하는 변 위의 격자점은 어느 쪽으로 계산해도 같은 값."""
verts = np.array(
[[0.0, 0.0, 0.0], [4.0, 0.0, 8.0], [4.0, 4.0, 20.0], [0.0, 4.0, 12.0]]
) # z = 2x + 3y
tris = np.array([[0, 1, 2], [0, 2, 3]])
x_coords, y_coords = _axes(0.0, 4.0, 0.0, 4.0)
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
xx, yy = np.meshgrid(x_coords, y_coords)
assert np.allclose(z_grid, 2.0 * xx + 3.0 * yy, atol=1e-9)
def test_degenerate_triangle_is_skipped():
"""면적 0 삼각형이 섞여 있어도 죽지 않고 나머지를 채운다."""
verts = np.array(
[
[0.0, 0.0, 1.0],
[4.0, 0.0, 1.0],
[0.0, 4.0, 1.0],
[1.0, 1.0, 99.0],
[2.0, 2.0, 99.0],
[3.0, 3.0, 99.0],
]
)
tris = np.array([[0, 1, 2], [3, 4, 5]]) # 두 번째는 일직선
x_coords, y_coords = _axes(0.0, 4.0, 0.0, 4.0)
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
filled = np.isfinite(z_grid)
assert filled.any()
assert np.allclose(z_grid[filled], 1.0)
def test_triangle_smaller_than_cell_still_lands():
"""격자 간격보다 작은 삼각형도 자기가 덮는 격자점을 채운다 (실데이터는 m²당 13.7면)."""
verts = np.array([[1.6, 1.6, 7.0], [2.4, 1.6, 7.0], [2.0, 2.4, 7.0]])
tris = np.array([[0, 1, 2]])
x_coords, y_coords = _axes(0.0, 4.0, 0.0, 4.0)
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
assert z_grid[2, 2] == pytest.approx(7.0)
assert np.isnan(z_grid[0, 0])
def test_float32_utm_axis_does_not_drop_cells():
"""float32 축(UTM 좌표) — 등간격 환산으로 인덱스를 내면 셀을 놓친다.
`_grid_axes`가 float32를 쓰는데 18만대 좌표에서 해상도가 0.015625m라 간격이
0.984~1.0으로 흔들린다. 등간격 가정 시 최대 0.81셀 어긋나 실측 8,819셀이
비었다(2026-08-17). 축을 직접 탐색해야 한다.
"""
x_coords = np.linspace(183433.6, 183805.8, 374, dtype=np.float32)
y_coords = np.linspace(489168.8, 489570.2, 403, dtype=np.float32)
# 격자 전체를 덮는 큰 삼각형 2개 → 모든 셀이 채워져야 한다.
x0, x1 = float(x_coords[0]) - 5.0, float(x_coords[-1]) + 5.0
y0, y1 = float(y_coords[0]) - 5.0, float(y_coords[-1]) + 5.0
corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
verts = np.array([[x, y, _plane(x, y)] for x, y in corners])
tris = np.array([[0, 1, 2], [0, 2, 3]])
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
assert np.isfinite(z_grid).all(), f"빈 셀 {int((~np.isfinite(z_grid)).sum()):,}개"
xx, yy = np.meshgrid(np.asarray(x_coords, np.float64), np.asarray(y_coords, np.float64))
assert np.abs(z_grid - _plane(xx, yy)).max() < 1e-6
def test_small_triangles_on_float32_axis_cover_every_cell():
"""실데이터처럼 격자보다 작은 삼각형이 촘촘할 때도 누락이 없어야 한다."""
x_coords = np.linspace(183433.6, 183463.6, 31, dtype=np.float32)
y_coords = np.linspace(489168.8, 489198.8, 31, dtype=np.float32)
# 0.5m 간격 정점망을 사각형→삼각형 2개로 쪼개 촘촘한 TIN을 흉내낸다.
gx = np.arange(float(x_coords[0]) - 1.0, float(x_coords[-1]) + 1.5, 0.5)
gy = np.arange(float(y_coords[0]) - 1.0, float(y_coords[-1]) + 1.5, 0.5)
mx, my = np.meshgrid(gx, gy)
verts = np.column_stack([mx.ravel(), my.ravel(), _plane(mx.ravel(), my.ravel())])
n_col = len(gx)
tris = []
for r in range(len(gy) - 1):
for c in range(n_col - 1):
p00, p10 = r * n_col + c, r * n_col + c + 1
p01, p11 = (r + 1) * n_col + c, (r + 1) * n_col + c + 1
tris.append([p00, p10, p11])
tris.append([p00, p11, p01])
z_grid = rasterize_triangle_mesh(verts, np.array(tris), x_coords, y_coords)
assert np.isfinite(z_grid).all(), f"빈 셀 {int((~np.isfinite(z_grid)).sum()):,}개"
xx, yy = np.meshgrid(np.asarray(x_coords, np.float64), np.asarray(y_coords, np.float64))
assert np.abs(z_grid - _plane(xx, yy)).max() < 1e-6
def test_descending_axis_is_supported():
"""내림차순 축(예: 북쪽이 위인 y축)도 같은 결과를 낸다."""
corners = [(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)]
verts = np.array([[x, y, _plane(x, y)] for x, y in corners])
tris = np.array([[0, 1, 2], [0, 2, 3]])
x_coords = np.linspace(0.0, 4.0, 5)
y_desc = np.linspace(4.0, 0.0, 5)
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_desc)
xx, yy = np.meshgrid(x_coords, y_desc)
assert np.isfinite(z_grid).all()
assert np.allclose(z_grid, _plane(xx, yy), atol=1e-9)
def test_empty_mesh_returns_all_nan():
x_coords, y_coords = _axes(0.0, 3.0, 0.0, 3.0)
z_grid = rasterize_triangle_mesh(
np.zeros((0, 3)), np.zeros((0, 3), dtype=np.int64), x_coords, y_coords
)
assert z_grid.shape == (len(y_coords), len(x_coords))
assert np.isnan(z_grid).all()