perf(B04): 등고선 피처 풀기를 한 번만 (격자와 무관)

PLAN 0-10. 「등고선 굽기」가 확장 8회 합 19s 였는데, 실측해 보니 굽기 자체는 0.8~2.0s
이고 나머지는 **피처 4,200개를 회차마다 다시 `shape()` 로 푸는 값**이었음. 푸는 결과는
격자와 무관하므로 같은 목록·같은 하한이면 그대로 쓴다(목록 객체를 함께 들고 있어 id 가
다른 목록에 재사용되지 않음, 최근 2벌만 보관).

실측 — 같은 격자 연속 4회: 2.66 · 1.90 · 1.99 · 2.03s (2회차부터 파싱 재사용), 결과
배열은 4회 모두 동일. `build_contour_descent` 전체는 옛 코드 12.8s → 7.2s 이며
band_elevation·valid·receiver·step_length·azimuth·levels 전부 `np.array_equal` 동일.

안 한 것 — 격자가 커질 때 옛 굽기를 옮겨 붙이고 **테두리만** 굽는 안은 만들어 재 봤으나
`rasterize` 호출당 고정 비용 때문에 오히려 3.2s → 10.6s 로 느렸고, 창에 걸치는 선만
넘기도록 고쳐도 3.8s 로 손해였음(창 4개 × 166단). 그래서 걷어내고 파싱 캐시만 남김.
`levels` 는 피처 전체에서 나오므로 격자와 무관하다는 것도 확인함.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 05:48:38 +09:00
co-authored by Claude Opus 5
parent f1834465c8
commit 1553f23c0d
@@ -72,12 +72,25 @@ class ContourDescent:
levels: list[float] # 사용된 등고 표고(내림차순) levels: list[float] # 사용된 등고 표고(내림차순)
def rasterize_contours( _LINES_CACHE: list[tuple[Any, int, float | None, dict[float, list[Any]]]] = []
spec: GridSpec,
contour_features: list[dict[str, Any]],
elevation_floor_m: float | None = None, def _lines_by_level(
) -> tuple[np.ndarray, list[float]]: contour_features: list[dict[str, Any]], elevation_floor_m: float | None
"""등고선을 격자에 굽는다. 셀마다 그 위를 지나는 등고 라인의 표고(없으면 NaN).""" ) -> dict[float, list[Any]]:
"""등고선 피처를 표고별 선 묶음으로 푼다 — **격자와 무관**하므로 한 번만 푼다.
확장 회차마다 다시 부르는데 피처 4,200개를 매번 `shape()` 로 푸는 비용이 그대로
붙었다. 같은 목록·같은 하한이면 그대로 돌려준다(목록 객체를 함께 들고 있어 id 가
다른 목록에 재사용되지 않는다).
"""
for holder, count, floor, cached in _LINES_CACHE:
if (
holder is contour_features
and count == len(contour_features)
and floor == elevation_floor_m
):
return cached
by_level: dict[float, list[Any]] = {} by_level: dict[float, list[Any]] = {}
for feature in contour_features: for feature in contour_features:
geometry = feature.get("geometry") geometry = feature.get("geometry")
@@ -96,7 +109,18 @@ def rasterize_contours(
if line.length < DRAINAGE_CONTOUR_MIN_LENGTH_M: if line.length < DRAINAGE_CONTOUR_MIN_LENGTH_M:
continue continue
by_level.setdefault(float(elevation), []).append(line) by_level.setdefault(float(elevation), []).append(line)
_LINES_CACHE.append((contour_features, len(contour_features), elevation_floor_m, by_level))
del _LINES_CACHE[:-2]
return by_level
def rasterize_contours(
spec: GridSpec,
contour_features: list[dict[str, Any]],
elevation_floor_m: float | None = None,
) -> tuple[np.ndarray, list[float]]:
"""등고선을 격자에 굽는다. 셀마다 그 위를 지나는 등고 라인의 표고(없으면 NaN)."""
by_level = _lines_by_level(contour_features, elevation_floor_m)
burned = np.full((spec.n_rows, spec.n_cols), np.nan, dtype=np.float32) burned = np.full((spec.n_rows, spec.n_cols), np.nan, dtype=np.float32)
levels = sorted(by_level, reverse=True) levels = sorted(by_level, reverse=True)
transform = grid_transform(spec) transform = grid_transform(spec)