Files
Aislo/resources/tester/test_616_zero_order_fast.py
T
eomsangdonandClaude Opus 5.5 66f9c68a1e perf(D03): 0차곡 = 창고 격자 · 도엽 선 미리 뽑아 이어 붙임 · 무거운 셈 일꾼 (PLAN 616-1 · 616-2 · 616-3)
- 0차곡 셈 = 사업 DEM 대신 창고 격자(받은 범위 도엽 누적) · 옛 격자 통째 갈래 걷음
- 기본 임계 선 = 도엽마다 창고 `zero_lines_<epsg>_2m.npz`(여유 200 m 에서 뽑아 끝점 도엽만 · 단순화 끝) · 요청 = 이어 붙여 받은 범위로 자름(파생 저장본 안 씀) · 다른 임계 = 누적 이어 붙여 셈 · 저장본
- 창고 누적 열쇠 판 그대로(ACC_VERSION 2 · 전국 누적 다시 셈 없음) · 파생 판 3
- compute=False = 셈 필요하면 None · 서버 본체 밖 일꾼 하나(`heavy_pool` · 띄울 때 빈 __main__ · 서버 끝나면 따라 끝남)
- 미리 계산 단계 zero_lines 덧(zero 뒤 · 기본 단계에 듦)
- 시험 test_616 새로 · test_608 반 칸 어긋난 DEM 통째 셈 확인 걷음(갈래 없어짐) · test_584_1 사업 DEM 통째 0차곡 겹 시험 걷음(창고 길로 바뀜 · 616 시험이 대신)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:18:43 +09:00

190 lines
8.0 KiB
Python

"""PLAN 616 — 0차곡 첫 계산 빠르게: 창고 격자에서 셈(616-1) · 도엽 선 미리 뽑아 이어 붙임(616-2) ·
무거운 셈 = 서버 밖 일꾼(616-3) · 급경사 값 그림 바이너리(616-4).
./venv/Scripts/python.exe -m pytest resources/tester/test_616_zero_order_fast.py -q
"""
from __future__ import annotations
import base64
import os
import sys
from pathlib import Path
import numpy as np
import pytest
import shapely
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from common_util import common_util_terrain_store as store # noqa: E402
from common_util.common_util_atomic import atomic_write_npz # noqa: E402
from common_util.common_util_json import atomic_write_json # noqa: E402
from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers # noqa: E402
from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zo # noqa: E402
from tools import precompute_terrain as tool # noqa: E402
EPSG = 5187
WEST, EAST = "36901074", "36901075"
CELL = 10.0 # 시험은 굵은 칸(여유 200 m = 20 칸 · 기본 임계 = 10 ~ 100 칸)
def _height(x, y):
"""기운 면 + 골 · 마루 — 웅덩이 없음 · 작은 집수."""
return 0.08 * x + 6.0 * np.sin(y / 38.0) + 2.0 * np.sin(x / 55.0)
def _put_sheet(no: str) -> None:
ix0, iy0, own = store._owned(no, EPSG, CELL)
rows, cols = own.shape
gx, gy = np.meshgrid((np.arange(cols) + ix0 + 0.5) * CELL, (np.arange(rows) + iy0 + 0.5) * CELL)
z = _height(gx - 300000.0, gy).astype(np.float32)
z[~own] = np.nan
npz, key_path, _ = store._names(no, EPSG, CELL)
atomic_write_npz(npz, z=z, ix0=np.int64(ix0), iy0=np.int64(iy0), cell_m=np.float64(CELL))
atomic_write_json(key_path, {**store._key(EPSG, CELL, {}), "source": "gpkg", "seconds": 1})
@pytest.fixture
def shared(tmp_path, monkeypatch):
"""임시 창고 · 칸 10 m · d8 부름 셈."""
monkeypatch.setattr(store, "TERRAIN_ROOT", tmp_path / "terrain")
monkeypatch.setattr(zo, "ZERO_ORDER_CELL_M", CELL)
calls = []
real = zo.d8_accumulation
monkeypatch.setattr(zo, "d8_accumulation", lambda *a: calls.append(1) or real(*a))
return calls
def _models(tmp_path: Path, shift: float = 0.0) -> Path:
"""두 도엽 가운데를 덮는 사업 DEM(칸 = 시험 칸 · 값은 안 씀 · 범위만)."""
grid = store.load_terrain(EPSG, sheets=[WEST, EAST], cell_m=CELL)
x, y = grid["x"][30:-30] + shift, grid["y"][30:-30]
models = tmp_path / f"models{shift:g}"
models.mkdir()
np.savez(
models / f"dtm_sheet_{zo.SHEET_SURFACE_DEFAULT_METHOD}.npz",
x=x,
y=y,
z=np.zeros((len(y), len(x))),
valid_mask=np.ones((len(y), len(x)), bool),
resolution=np.array([CELL], np.float32),
)
return models
def test_sheet_lines_join_equals_box(shared, tmp_path) -> None:
"""도엽 선(여유 200 m 에서 뽑아 끝점 도엽만) 이어 붙임 = 두 도엽 통째 상자에서 뽑은 선."""
_put_sheet(WEST)
_put_sheet(EAST)
paths = [zo.sheet_lines(no, EPSG, CELL) for no in (WEST, EAST)]
joined = [g for p in paths for g in zo._read_lines(p)]
grid = store.load_terrain(EPSG, sheets=[WEST, EAST], cell_m=CELL)
half = CELL / 2
box = (grid["x"][0] - half, grid["y"][0] - half, grid["x"][-1] + half, grid["y"][-1] + half)
accs = [zo._acc_names(no, EPSG, CELL)[0] for no in (WEST, EAST)]
whole = zo._box_lines(accs, EPSG, box, CELL, zo.ZERO_ORDER_MIN_M2, zo.ZERO_ORDER_MAX_M2)
assert len(whole) >= 20
# 여유 200 m 보다 긴 선(시험 지형 = 동서로 긴 골)만 창 끝에서 잘려 다를 수 있음 — 1 % 안
assert abs(len(joined) - len(whole)) <= 0.01 * len(whole)
total = sum(g.length for g in whole)
assert abs(sum(g.length for g in joined) - total) <= 0.01 * total
keys = {tuple(map(tuple, shapely.get_coordinates(g).round(3))) for g in joined}
assert len(keys) == len(joined) # 겹쳐 뽑힌 선은 한 도엽에만
def test_layer_default_reads_lines_light(shared, tmp_path) -> None:
"""기본 임계 = 창고 도엽 선 이어 붙여 자름 — 창고에 없으면 compute=False 가 None(일꾼 몫) ·
있으면 d8 · 선 뽑기 없이 바로."""
calls = shared
_put_sheet(WEST)
_put_sheet(EAST)
models = _models(tmp_path)
assert zo.zero_order_layer(tmp_path, models, EPSG, compute=False) is None
assert calls == []
first = zo.zero_order_layer(tmp_path, models, EPSG)
assert first["count"] >= 10 and first["cell_m"] == CELL
assert len(calls) == 2 # 도엽 둘 누적 한 번씩
light = zo.zero_order_layer(tmp_path, models, EPSG, compute=False)
assert (light["count"], light["length_m"]) == (first["count"], first["length_m"])
assert len(calls) == 2
assert light["collection"]["features"][0]["geometry"]["type"] == "LineString"
# 받은 범위 밖 선은 잘림 — 모든 점이 범위 안
with np.load(models / f"dtm_sheet_{zo.SHEET_SURFACE_DEFAULT_METHOD}.npz") as dem:
x0, x1 = dem["x"][0] - CELL / 2, dem["x"][-1] + CELL / 2
from pyproj import Transformer
to_m = Transformer.from_crs("EPSG:4326", f"EPSG:{EPSG}", always_xy=True)
pts = np.array(
[p for f in light["collection"]["features"] for p in f["geometry"]["coordinates"]]
)
mx, _ = to_m.transform(pts[:, 0], pts[:, 1])
assert mx.min() >= x0 - 0.01 and mx.max() <= x1 + 0.01
def test_layer_custom_threshold_saved_and_grid_free(shared, tmp_path) -> None:
"""다른 임계 = 창고 누적에서 셈(사업 DEM 격자 안 씀 · 반 칸 어긋나도 창고) · 둘째 = 저장본."""
calls = shared
_put_sheet(WEST)
_put_sheet(EAST)
models = _models(tmp_path)
cells = CELL * CELL
assert zo.zero_order_layer(tmp_path, models, EPSG, 5 * cells, 40 * cells, False) is None
first = zo.zero_order_layer(tmp_path, models, EPSG, 5 * cells, 40 * cells)
assert first["count"] >= 1 and len(calls) == 2
again = zo.zero_order_layer(tmp_path, models, EPSG, 5 * cells, 40 * cells, False)
assert again == first # 저장본
shifted = zo.zero_order_layer(
tmp_path, _models(tmp_path, CELL / 2), EPSG, 5 * cells, 40 * cells
)
assert shifted["count"] >= 1 and len(calls) == 2 # 옛 격자 갈래 없음 = 누적 다시 안 셈
def test_lines_recomputed_when_neighbor_acc_appears(shared) -> None:
_put_sheet(WEST)
_put_sheet(EAST)
path = zo.sheet_lines(WEST, EPSG, CELL) # 동쪽 누적 없음 = 빼고 셈
stamp = path.stat().st_mtime_ns
assert zo.sheet_lines(WEST, EPSG, CELL, compute=False) == path
zo.sheet_accumulation(EAST, EPSG, CELL)
with pytest.raises(zo.NeedsCompute):
zo.sheet_lines(WEST, EPSG, CELL, compute=False)
assert zo.sheet_lines(WEST, EPSG, CELL).stat().st_mtime_ns != stamp
def test_acc_key_version_unchanged() -> None:
"""누적 열쇠 판을 올리면 전국 미리 계산 누적을 통째 다시 셈 — 616 은 안 올림."""
assert zo.ACC_VERSION == 2
def test_precompute_zero_lines_step(shared, monkeypatch) -> None:
_put_sheet(WEST)
_put_sheet(EAST)
monkeypatch.setattr(tool, "CELL_M", CELL)
assert "zero_lines" in tool.STEPS and tool.STEPS.index("zero_lines") == 2
_, _, info = tool.run_step("zero_lines", WEST)
assert info["ok"] and info["fresh"] and not info["empty"]
_, _, again = tool.run_step("zero_lines", WEST)
assert again["ok"] and not again["fresh"]
def test_raster_bytes_roundtrip() -> None:
png = b"\x89PNG\r\n\x1a\nfake"
value = {"image": "data:image/png;base64," + base64.b64encode(png).decode(), "counts": [1, 2]}
got, meta = layers.raster_bytes(value)
assert got == png and meta == {"counts": [1, 2]}
def test_heavy_pool_runs_outside() -> None:
"""일꾼 = 다른 프로세스 · 띄울 때 __main__ 을 되돌려 놓음."""
main = sys.modules["__main__"]
pool = zo.heavy_pool()
try:
assert sys.modules["__main__"] is main
assert pool.submit(os.getpid).result(timeout=120) != os.getpid()
assert zo.heavy_pool() is pool
finally:
pool.shutdown()
zo._POOL = None