fix(B04): NURBS 곡면 발산 — 평활계수 완화·표고 범위 방어·등고선 레벨 상한 순서

csf-nurbs 등고선 캐시가 Maximum allowed size exceeded로 실패하던 문제.
평활계수 s가 제어점당 잔차제곱 0.01(RMS 0.1m)로 너무 빡빡해 FITPACK이
제어점 수(122x113)보다 많은 knot(126x117)을 밀어넣고 계수가 발산했다.
실측 표고: csf 1e105, grid_min_z 1e10. grid_min_z는 footprint 안쪽만
보고 z 범위를 잡아 조용히 통과하고 있었다.

- ModelContext에 fit_nurbs_spline/evaluate_nurbs_spline 신설. 모델 빌더와
  등고선 엔진이 같은 곡면을 각각 만들면서 s 식이 두 곳에 중복돼 있었다.
- NURBS_RESIDUAL_PER_CONTROL_POINT = 1.0 (RMS 1m). knot이 17~35로 떨어지고
  세 지면 필터 모두 데이터 표고 범위 안에 머문다. NURBS는 평활 곡면
  표현이라 이 정도 완화가 맞다.
- 평가 결과가 제어 표고 범위(±50% 여유)를 벗어나면 잘라내고 경고를 남긴다.
  방치하면 float32 캐스팅에서 inf가 되어 프리뷰 색상(nan)과 등고선까지 번진다.
- extract_contours_from_grid: 레벨 수 상한 검사를 np.arange 앞으로 옮겼다.
  뒤에 있어서 상한이 무용지물이었다 — 배열이 만들어지기 전에 터진다.

확인: 3필터 모두 곡면이 제어 표고 범위 내(grid_min_z 503.6~573.1,
csf 504.7~552.3, pmf 504.6~551.8), 등고선 0.55~1.64초에 정상 산출.
표고 범위 1e12 격자도 예외 없이 처리. tmp/tests 59건 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 02:23:42 +09:00
co-authored by Claude Opus 5
parent f4a9791662
commit 9a5496c47f
3 changed files with 85 additions and 24 deletions
+18 -14
View File
@@ -10,7 +10,7 @@ from pathlib import Path
from typing import Any
import numpy as np
from scipy.interpolate import RBFInterpolator, RectBivariateSpline
from scipy.interpolate import RBFInterpolator
from skimage import measure
# 등고선 캐시 형식/추출 규칙이 바뀔 때 증가시킨다.
@@ -36,14 +36,17 @@ def extract_contours_from_grid(
z_min = float(np.min(z_grid[finite_mask]))
z_max = float(np.max(z_grid[finite_mask]))
span = z_max - z_min
if span <= 0:
return []
# 레벨 수 상한은 arange **앞에서** 건다. 뒤에 두면 표고 범위가 비정상적으로 넓을 때
# np.arange가 배열을 만들다 "Maximum allowed size exceeded"로 먼저 터진다.
if span / interval > 500:
interval = span / 100.0
start_level = np.ceil(z_min / interval) * interval
levels = np.arange(start_level, z_max, interval)
if len(levels) == 0:
return []
if len(levels) > 500:
new_interval = (z_max - z_min) / 100.0
levels = np.arange(np.ceil(z_min / new_interval) * new_interval, z_max, new_interval)
interval = new_interval
contours_geojson_list: list[dict[str, Any]] = []
@@ -398,16 +401,15 @@ def extract_contours(
)
if representation == "bspline_surface":
# 모델 빌더와 같은 곡면을 다시 만든다 — 평활 계수·발산 방어를 공유한다.
from B04_PreProcess.B04_PreProcess_Engine_ModelContext import (
evaluate_nurbs_spline,
fit_nurbs_spline,
)
control_x, control_y, control_z = data["control_x"], data["control_y"], data["control_z"]
degree = int(data["degree"][0])
spline = RectBivariateSpline(
control_y,
control_x,
control_z,
kx=min(degree, len(control_y) - 1),
ky=min(degree, len(control_x) - 1),
s=float(len(control_x) * len(control_y)) * 0.01,
)
spline, z_range = fit_nurbs_spline(control_x, control_y, control_z, degree)
x_coords, y_coords = _grid_axes(
float(control_x[0]),
float(control_x[-1]),
@@ -415,7 +417,9 @@ def extract_contours(
float(control_y[-1]),
target_grid_m,
)
z_grid = np.asarray(spline(y_coords, x_coords), dtype=np.float32)
z_grid = evaluate_nurbs_spline(
spline, y_coords, x_coords, z_range, Path(model_npz_path).stem
).astype(np.float32)
valid_mask = _apply_footprint(
model_npz_path, x_coords, y_coords, np.ones_like(z_grid, dtype=bool)
)