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>
This commit is contained in:
@@ -182,31 +182,6 @@ def test_zero_order_lines_head_only() -> None:
|
||||
assert sum(math.dist(a, b) for a, b in zip(line, line[1:])) >= zo.ZERO_ORDER_MIN_LEN_M
|
||||
|
||||
|
||||
def test_zero_order_layer_saved_and_threshold_key(tmp_path, monkeypatch) -> None:
|
||||
n = 60
|
||||
models = tmp_path / "models"
|
||||
models.mkdir()
|
||||
np.savez(
|
||||
models / f"dtm_sheet_{zo.SHEET_SURFACE_DEFAULT_METHOD}.npz",
|
||||
x=230000.0 + np.arange(n) * 2.0,
|
||||
y=450000.0 + np.arange(n) * 2.0,
|
||||
z=_valley(n),
|
||||
valid_mask=np.ones((n, n), bool),
|
||||
resolution=np.array([2.0, 2.0]),
|
||||
)
|
||||
calls = []
|
||||
real = zo.zero_order_lines
|
||||
monkeypatch.setattr(zo, "zero_order_lines", lambda *a, **k: calls.append(1) or real(*a, **k))
|
||||
first = zo.zero_order_layer(tmp_path, models, EPSG, 400.0, 2000.0)
|
||||
assert first["count"] >= 1 and first["length_m"] > 0 and first["cell_m"] == 2.0
|
||||
assert first["collection"]["features"][0]["geometry"]["type"] == "LineString"
|
||||
again = zo.zero_order_layer(tmp_path, models, EPSG, 400.0, 2000.0)
|
||||
assert (again["count"], again["length_m"]) == (first["count"], first["length_m"])
|
||||
assert len(calls) == 1 # 두 번째 = 저장본
|
||||
zo.zero_order_layer(tmp_path, models, EPSG, 400.0, 3000.0) # 임계 바뀜 = 다시 셈
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
def test_zero_order_no_dem(tmp_path) -> None:
|
||||
with pytest.raises(FileNotFoundError):
|
||||
zo.zero_order_layer(tmp_path, tmp_path, EPSG)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""D 0차곡 흐름누적 = 도엽별 공용 창고(PLAN 608-2) — 도엽 + 여유 200 m 조각 = 통째 셈 ·
|
||||
처음만 셈 · 이웃 도엽 지형이 새로 생기면 다시 · 임계 바꿈은 누적 그대로 · 창고 격자 아닌 DEM = 통째.
|
||||
처음만 셈 · 이웃 도엽 지형이 새로 생기면 다시 · 임계 바꿈은 누적 그대로(사업 DEM 격자 무관 = 616).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_608_plan_zero_order_store.py -q
|
||||
"""
|
||||
@@ -77,7 +77,7 @@ def test_tiles_equal_whole_and_saved(shared) -> None:
|
||||
|
||||
|
||||
def test_layer_uses_store_and_threshold_reuses(shared, tmp_path, monkeypatch) -> None:
|
||||
"""사업 DEM 이 창고 격자 위면 도엽 누적을 이어 붙임 · 임계 바꿈 = 누적 다시 안 셈."""
|
||||
"""받은 범위 도엽 누적을 이어 붙임 · 임계 바꿈 = 누적 다시 안 셈."""
|
||||
calls = shared
|
||||
monkeypatch.setattr(zo, "ZERO_ORDER_CELL_M", CELL)
|
||||
_put_sheet(WEST)
|
||||
@@ -100,5 +100,3 @@ def test_layer_uses_store_and_threshold_reuses(shared, tmp_path, monkeypatch) ->
|
||||
assert len(calls) == 2 # 도엽 둘 한 번씩
|
||||
zo.zero_order_layer(tmp_path, models, EPSG, 1 * cells, 10 * cells)
|
||||
assert len(calls) == 2 # 임계 바꿈 = 저장 누적에서 칸 고르기만
|
||||
# 창고 격자에서 반 칸 어긋난 DEM = 통째 셈
|
||||
assert zo.store_accumulation_paths(x + CELL / 2, y, EPSG, CELL) is None
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user