perf(D03 0차곡): D8 평지 이어 감 = 같은 높이 이웃 표 한 번 · 안 흐르는 후보 칸만 돌림 — 결과 비트 동일 · 장당 2~27 배 (PLAN 617-1 · 요구 504 · 506)

- 방향 차례 · 갱신 차례 그대로 · ZERO_ORDER_VERSION · 창고 열쇠 그대로
- 시험 test_617_1_d8_flat: 옛 꼴과 합성 격자 대조 + 창고 0차곡 도엽 열 장 다시 셈 = 저장본(읽기만)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 10:25:49 +09:00
co-authored by Claude Opus 5.5
parent 2f90727ca9
commit c5a22d6407
2 changed files with 165 additions and 11 deletions
+23 -11
View File
@@ -78,17 +78,29 @@ def d8_accumulation(z: np.ndarray, valid: np.ndarray) -> np.ndarray:
best = np.where(better, drop, best)
receiver = np.where(better, index + dr * cols + dc, receiver)
done = (receiver >= 0) | edge | ~valid
while True:
grew = False
for dr, dc in _D8:
take = ~done & _shift(done & valid, dr, dc, False)
take &= _shift(filled, dr, dc, np.nan) == filled
if take.any():
receiver[take] = index[take] + dr * cols + dc
done |= take
grew = True
if not grew:
break
# 평지 이어 감 — 같은 높이 이웃 표는 한 번 · 아직 안 흐르는 후보 칸만 돌림(방향 · 갱신 차례 그대로)
eq = [_shift(filled, dr, dc, np.nan) == filled for dr, dc in _D8]
cand = np.flatnonzero(~done & np.any(eq, axis=0))
done_f, valid_f, rec_f = done.ravel(), valid.ravel(), receiver.ravel()
r, c = np.divmod(cand, cols)
nbr, ok = [], []
for k, (dr, dc) in enumerate(_D8):
inb = (r + dr >= 0) & (r + dr < rows) & (c + dc >= 0) & (c + dc < cols)
ok.append(inb & eq[k].ravel()[cand])
nbr.append(np.where(inb, cand + dr * cols + dc, 0))
while True:
grew = False
for k, (dr, dc) in enumerate(_D8):
take = ok[k] & ~done_f[cand] & done_f[nbr[k]] & valid_f[nbr[k]]
if take.any():
hit = cand[take]
rec_f[hit] = hit + dr * cols + dc
done_f[hit] = True
grew = True
if not grew:
break
keep = ~done_f[cand]
cand, nbr, ok = cand[keep], [n[keep] for n in nbr], [o[keep] for o in ok]
down = receiver.ravel().tolist()
acc = valid.astype(float).ravel().tolist()
upstream = np.bincount(receiver[receiver >= 0], minlength=rows * cols).tolist()
+142
View File
@@ -0,0 +1,142 @@
"""D 0차곡 D8 평지 이어 감 고침(PLAN 617-1) — 옛 꼴(통격자 바퀴)과 새 꼴(후보 칸 목록) 비트 동일 ·
합성 격자(넓은 평지 · 계단 · 빈 칸 · 가장자리) + 창고 0차곡 도엽 다시 셈 = 저장본(읽기만).
./venv/Scripts/python.exe -m pytest resources/tester/test_617_1_d8_flat.py -q
"""
import json
import math
import numpy as np
import pytest
from common_util import common_util_terrain_store as store
from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zo
_D8 = zo._D8
_shift = zo._shift
def _old_d8(z: np.ndarray, valid: np.ndarray) -> np.ndarray:
"""617-1 전 꼴 그대로(대조용)."""
from skimage.morphology import reconstruction
rows, cols = z.shape
height = np.where(valid, np.asarray(z, float), np.nan)
floor = np.nanmin(height) if valid.any() else 0.0
edge = valid & ~np.all([_shift(valid, dr, dc, False) for dr, dc in _D8], axis=0)
ground = np.where(valid, height, floor)
seed = np.where(edge | ~valid, ground, ground.max())
filled = np.where(valid, reconstruction(seed, ground, method="erosion"), np.nan)
index = np.arange(rows * cols).reshape(rows, cols)
best = np.zeros(z.shape)
receiver = np.full(z.shape, -1, np.int64)
with np.errstate(invalid="ignore"):
for dr, dc in _D8:
drop = (filled - _shift(filled, dr, dc, np.nan)) / math.hypot(dr, dc)
better = drop > best
best = np.where(better, drop, best)
receiver = np.where(better, index + dr * cols + dc, receiver)
done = (receiver >= 0) | edge | ~valid
while True:
grew = False
for dr, dc in _D8:
take = ~done & _shift(done & valid, dr, dc, False)
take &= _shift(filled, dr, dc, np.nan) == filled
if take.any():
receiver[take] = index[take] + dr * cols + dc
done |= take
grew = True
if not grew:
break
down = receiver.ravel().tolist()
acc = valid.astype(float).ravel().tolist()
upstream = np.bincount(receiver[receiver >= 0], minlength=rows * cols).tolist()
stack = [i for i, n in enumerate(upstream) if n == 0]
while stack:
i = stack.pop()
j = down[i]
if j >= 0:
acc[j] += acc[i]
upstream[j] -= 1
if upstream[j] == 0:
stack.append(j)
return np.asarray(acc).reshape(rows, cols)
def _grids():
rng = np.random.default_rng(617)
yy, xx = np.mgrid[0:60, 0:80]
flat = np.full((60, 80), 5.0) # 넓은 평지(통째)
basin = flat.copy()
basin[10:50, 10:70] = 3.0 # 메운 웅덩이 = 넓은 평지
steps = np.floor(xx / 9.0) + np.floor(yy / 13.0) # 계단(층마다 평지)
noisy = np.round(rng.normal(0, 1, (60, 80)) + yy * 0.05, 0) # 같은 높이 많음
slope = 0.3 * xx + np.sin(yy / 5.0)
out = []
for z in (flat, basin, steps, noisy, slope):
out.append((z, np.ones(z.shape, bool)))
holes = rng.random(z.shape) > 0.1 # 빈 칸
holes[:, :3] = False # 빈 띠 가장자리
out.append((z, holes))
one = np.zeros((1, 1))
out.append((one, np.ones((1, 1), bool)))
out.append((np.zeros((3, 5)), np.zeros((3, 5), bool))) # 모두 빈 칸
return out
@pytest.mark.parametrize("z,valid", _grids())
def test_new_equals_old_synthetic(z, valid) -> None:
assert np.array_equal(zo.d8_accumulation(z, valid), _old_d8(z, valid))
def _stored_tiles(limit: int = 10):
"""열쇠가 지금 지형 파일과 맞는 창고 0차곡 도엽(고루 솎음)."""
found = sorted(store.TERRAIN_ROOT.glob("*/zero_acc_*_2m.key.json"))
step = max(len(found) // (limit * 3), 1)
for key_path in found[::step]:
key = json.loads(key_path.read_text(encoding="utf-8"))
if key.get("version") != zo.ZERO_ORDER_VERSION:
continue
epsg = int(key_path.name.split("_")[2])
dems = {}
for no, dem in key["dem"].items():
npz, dem_key, _ = store._names(no, epsg, zo.ZERO_ORDER_CELL_M)
try:
now = json.loads(dem_key.read_text(encoding="utf-8"))
except (OSError, ValueError):
break
if {k: v for k, v in now.items() if k != "seconds"} != dem or not npz.is_file():
break
dems[no] = npz
else:
yield key_path.parent.name, epsg, dems
limit -= 1
if limit == 0:
return
def test_store_tiles_recompute_equal() -> None:
"""창고 도엽 누적을 새 꼴로 다시 셈 = 저장본(읽기만 · 창고 안 씀)."""
tiles = list(_stored_tiles())
if not tiles:
pytest.skip("창고 0차곡 도엽 없음")
cell = zo.ZERO_ORDER_CELL_M
pad = math.ceil(zo.ZERO_ORDER_MARGIN_M / cell)
for no, epsg, dems in tiles:
with np.load(zo._acc_names(no, epsg, cell)[0]) as data:
want = np.asarray(data["acc"])
ix0, iy0 = int(data["ix0"]) - pad, int(data["iy0"]) - pad
rows, cols = want.shape[0] + 2 * pad, want.shape[1] + 2 * pad
z = np.full((rows, cols), np.nan, np.float32)
for npz in dems.values(): # load_terrain 이음과 같음(지형 다시 셈 길 없이)
with np.load(npz) as data:
px, py, pz = int(data["ix0"]), int(data["iy0"]), data["z"]
r0, c0 = max(py, iy0), max(px, ix0)
r1, c1 = min(py + pz.shape[0], iy0 + rows), min(px + pz.shape[1], ix0 + cols)
if r0 < r1 and c0 < c1:
src = pz[r0 - py : r1 - py, c0 - px : c1 - px]
np.copyto(z[r0 - iy0 : r1 - iy0, c0 - ix0 : c1 - ix0], src, where=np.isfinite(src))
acc = zo.d8_accumulation(z, np.isfinite(z))[pad:-pad, pad:-pad]
got = np.minimum(acc, zo.ACC_CAP).astype(np.uint16)
assert np.array_equal(got, want), no