- 새 common_util_terrain_laplace.py — B04 build_laplace 짝(같은 덧셈 차례 · float32) · build_contour_surface 가 부름 - 폐합 채움 = 수위 칸 상자 + 1 칸 덧댐 안 채움 · 묶음 find_objects 창 · 차례 그대로 · 쓰지 않는 _window 지움 - TERRAIN_ALGO 1 그대로 · B 파일 안 고침 - 짝 시험 test_617_2_laplace_mirror.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
5.8 KiB
Python
127 lines
5.8 KiB
Python
"""PLAN 617-2 — 공용 지형 빠른 판이 옛 길과 비트 동일인지(B04 짝).
|
|
|
|
① 라플라스 — B04 `build_laplace` 대 `common_util_terrain_laplace.laplace_surface` ·
|
|
짝 · 홀 크기 · 고정 칸 많음 · 적음 · NaN 칸.
|
|
② 폐합 채움 — 수위 상자 · 묶음 창 판 대 통격자 판(옛 셈 그대로 본뜸) · 고리가 격자 끝에 닿음 ·
|
|
겹 고리 · 안에 다른 수위.
|
|
|
|
./venv/Scripts/python.exe -m pytest resources/tester/test_617_2_laplace_mirror.py -q
|
|
"""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from B04_PreProcess.B04_PreProcess_Engine_SheetMethods import build_laplace, relax_laplace
|
|
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Grid import GridSpec
|
|
from common_util import common_util_terrain_surface as surf
|
|
from common_util.common_util_terrain_laplace import laplace_surface, relax_checker
|
|
|
|
|
|
def _burned(shape, fixed_share, seed):
|
|
rng = np.random.default_rng(seed)
|
|
burned = np.full(shape, np.nan, dtype=np.float32)
|
|
pick = rng.random(shape) < fixed_share
|
|
burned[pick] = rng.integers(0, 12, int(pick.sum())).astype(np.float32) * 5.0 + 100.0
|
|
return burned
|
|
|
|
|
|
@pytest.mark.parametrize("shape", [(40, 52), (41, 53), (40, 53), (41, 52), (1, 7), (2, 3)])
|
|
@pytest.mark.parametrize("fixed_share", [0.02, 0.4])
|
|
def test_laplace_matches_b04(shape, fixed_share):
|
|
burned = _burned(shape, fixed_share, seed=shape[0] * 100 + shape[1])
|
|
burned.flat[0], burned.flat[-1] = 100.0, 155.0 # 수위 둘 이상
|
|
spec = GridSpec(x_min=0.0, y_max=shape[0] * 2.0, cell_m=2.0, n_rows=shape[0], n_cols=shape[1])
|
|
old = build_laplace(spec, burned, None, 2.0)
|
|
new = laplace_surface(burned, 2.0)
|
|
assert new.dtype == old.dtype == np.float32
|
|
assert np.array_equal(old, new)
|
|
|
|
|
|
@pytest.mark.parametrize("shape", [(30, 30), (31, 29)])
|
|
def test_relax_keeps_nan_and_fixed(shape):
|
|
rng = np.random.default_rng(3)
|
|
base = rng.random(shape).astype(np.float32) * 50.0
|
|
base[rng.random(shape) < 0.05] = np.nan
|
|
fixed = rng.random(shape) < 0.1
|
|
old, new = base.copy(), base.copy()
|
|
relax_laplace(old, fixed, 37)
|
|
relax_checker(new, fixed, 37)
|
|
assert np.array_equal(old, new, equal_nan=True)
|
|
|
|
|
|
def _full_grid_fill(burned, present, surface, cell_m, interval_m):
|
|
"""옛 셈(PLAN 607-1 판) — 수위마다 통격자 채움 · 묶음마다 통격자 `components == id`."""
|
|
from scipy.ndimage import binary_dilation, binary_fill_holes, distance_transform_edt, label
|
|
|
|
band_cells, constrained = surf._BAND_CELLS, np.isfinite(burned)
|
|
limit, pad, eight = max(interval_m - 0.5, 0.5), surf._BAND_CELLS + 2, np.ones((3, 3), bool)
|
|
resolved = 0
|
|
for level in present:
|
|
mask = burned == level
|
|
interior = binary_fill_holes(mask) & ~mask
|
|
components, count = label(interior)
|
|
for component_id in range(1, count + 1):
|
|
whole = components == component_id
|
|
if (constrained & whole).any():
|
|
continue
|
|
rows, cols = np.flatnonzero(whole.any(axis=1)), np.flatnonzero(whole.any(axis=0))
|
|
win = (
|
|
slice(max(rows[0] - pad, 0), rows[-1] + pad + 1),
|
|
slice(max(cols[0] - pad, 0), cols[-1] + pad + 1),
|
|
)
|
|
component, line, local = whole[win], mask[win], surface[win]
|
|
loop = binary_dilation(component, structure=eight) & line
|
|
ring = binary_dilation(loop, structure=eight) & ~component & ~line
|
|
ring &= np.isfinite(local)
|
|
if not ring.any():
|
|
continue
|
|
direction = 1.0 if float(local[ring].mean()) < level else -1.0
|
|
inner = distance_transform_edt(component, sampling=cell_m)
|
|
outer = distance_transform_edt(~(loop | component), sampling=cell_m)
|
|
band = (outer > 0) & (outer <= band_cells * cell_m)
|
|
band &= np.isfinite(local) & ~component & ~line
|
|
slope = (
|
|
float(np.mean(np.abs(level - local[band].astype(np.float64)) / outer[band]))
|
|
if band.any()
|
|
else 0.0
|
|
)
|
|
if slope > 1e-3:
|
|
offset = np.minimum(slope * inner[component], limit)
|
|
else:
|
|
peak = float(inner.max())
|
|
offset = (limit / 2.0) * (inner[component] / peak) if peak > 0 else 0.0
|
|
local[component] = level + direction * offset
|
|
resolved += 1
|
|
return resolved
|
|
|
|
|
|
def _ellipse(cx, cy, rx, ry, n=64):
|
|
t = np.linspace(0.0, 2 * np.pi, n + 1)
|
|
return np.column_stack([cx + rx * np.cos(t), cy + ry * np.sin(t)])
|
|
|
|
|
|
@pytest.mark.parametrize("n_rows,n_cols", [(150, 200), (151, 199)])
|
|
def test_window_fill_matches_full_grid(n_rows, n_cols):
|
|
lines = [
|
|
(_ellipse(100, 150, 80, 60), 95.0),
|
|
(_ellipse(100, 150, 40, 30), 100.0), # 봉우리
|
|
(_ellipse(100, 150, 12, 9), 105.0), # 겹 봉우리
|
|
(_ellipse(300, 150, 60, 50), 105.0),
|
|
(_ellipse(300, 150, 25, 20), 100.0), # 웅덩이(같은 높이 다른 고리)
|
|
(_ellipse(4, 290, 30, 20), 100.0), # 격자 끝 · 모서리에 걸린 고리
|
|
(_ellipse(396, 6, 25, 25), 110.0),
|
|
(_ellipse(200, 60, 30, 20), 100.0),
|
|
(_ellipse(200, 60, 8, 6), 90.0), # 안에 다른 수위 → 건너뜀
|
|
(np.array([[0.0, 240.0], [400.0, 200.0]]), 95.0),
|
|
]
|
|
spec = GridSpec(x_min=0.0, y_max=n_rows * 2.0, cell_m=2.0, n_rows=n_rows, n_cols=n_cols)
|
|
a, b, z = surf.segments_from_lines(lines)
|
|
burned = surf.burn_segments(spec, a, b, z)
|
|
present = sorted(np.unique(burned[np.isfinite(burned)]).tolist())
|
|
base = laplace_surface(burned, 2.0, iterations=30)
|
|
old, new = base.copy(), base.copy()
|
|
count_old = _full_grid_fill(burned, present, old, 2.0, 5.0)
|
|
count_new = surf.resolve_enclosed_interiors(burned, present, new, 2.0, 5.0)
|
|
assert count_old == count_new >= 3
|
|
assert np.array_equal(old, new)
|