Merge remote-tracking branches 'origin/sub_laptop_3' and 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -75,7 +75,7 @@ const LATEST = {
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 100, y: 0 },
|
||||
],
|
||||
surface_params: { source_filter: "csf", method: "tin", smooth: true, contour_interval_m: 1 },
|
||||
surface_params: { source_filter: "ptd", method: "implicit", smooth: true, contour_interval_m: 1 },
|
||||
route_params: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from common_util.common_util_surface_confirmation import (
|
||||
from common_util.common_util_surface_sampler import build_surface_sampler, check_surface_keys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
LAS_PARAMS = {"source_filter": "csf", "method": "dtm", "smooth": True, "contour_interval_m": 1.0}
|
||||
LAS_PARAMS = {"source_filter": "ptd", "method": "dtm", "smooth": True, "contour_interval_m": 1.0}
|
||||
SHEET_PARAMS = {
|
||||
"source_filter": SHEET_ANALYSIS_FILTER,
|
||||
"method": "dtm",
|
||||
@@ -51,7 +51,7 @@ def _project(tmp_path: Path, *, las: bool, sheet: bool) -> Path:
|
||||
models = tmp_path / "B04_PreProcess" / "models"
|
||||
models.mkdir(parents=True)
|
||||
if las:
|
||||
_dtm(models / "dtm_csf_smooth.npz", LAS_Z)
|
||||
_dtm(models / "dtm_ptd_smooth.npz", LAS_Z)
|
||||
if sheet: # B04 처럼 원본 · 평활 둘 다(평활 쪽을 읽음)
|
||||
_dtm(models / f"dtm_{SHEET_ANALYSIS_FILTER}.npz", SHEET_Z - 1.0)
|
||||
_dtm(models / f"dtm_{SHEET_ANALYSIS_FILTER}_smooth.npz", SHEET_Z)
|
||||
|
||||
@@ -40,9 +40,9 @@ def runs() -> dict:
|
||||
caught: dict = {}
|
||||
real = engine.build_pool
|
||||
|
||||
def spy(graph, follow, max_grade, min_radius_m, measure, snap, **kw):
|
||||
def spy(graph, follow, max_grade, min_radius_m, measure, **kw):
|
||||
caught["measure"] = measure
|
||||
caught["pool"] = real(graph, follow, max_grade, min_radius_m, measure, snap, **kw)
|
||||
caught["pool"] = real(graph, follow, max_grade, min_radius_m, measure, **kw)
|
||||
return caught["pool"]
|
||||
|
||||
engine._POOLS.clear()
|
||||
|
||||
@@ -102,8 +102,8 @@ def _plant_like_b06(root: Path) -> None:
|
||||
stations = sections.generate_irregular_sections(
|
||||
root,
|
||||
ROUTE,
|
||||
"csf",
|
||||
"tin",
|
||||
"ptd",
|
||||
"implicit",
|
||||
False,
|
||||
extra_stations=sections.resolve_extra_stations(root, pipes),
|
||||
)
|
||||
@@ -113,8 +113,8 @@ def _plant_like_b06(root: Path) -> None:
|
||||
def _save(root: Path, screen: list[tuple[float, str]]) -> None:
|
||||
"""B05 [저장] — 화면은 관 · A군 목록만 실어 보냄."""
|
||||
request = RouteConfirmRequest(
|
||||
filter_key="csf",
|
||||
method="tin",
|
||||
filter_key="ptd",
|
||||
method="implicit",
|
||||
surface_model_id=1,
|
||||
irregular_stations=[{"chainage_m": c, "structure": s} for c, s in screen],
|
||||
)
|
||||
|
||||
@@ -53,8 +53,8 @@ def test_종단_보기를_열든_안_열든_같은_요청(got):
|
||||
assert got["closed"]["lines"] == got["open"]["lines"]
|
||||
body = _confirm_body(got["closed"]["lines"])
|
||||
assert body == {
|
||||
"filter_key": "csf",
|
||||
"method": "tin",
|
||||
"filter_key": "ptd",
|
||||
"method": "implicit",
|
||||
"smooth": True,
|
||||
"surface_model_id": 7,
|
||||
"irregular_stations": [],
|
||||
|
||||
@@ -60,12 +60,12 @@ def test_미리_뜨기는_안_쓰고_저장과_같은_횡단(tmp_path: Path, mon
|
||||
monkeypatch.setattr(sections, "build_surface_sampler", lambda *_a, **_k: _Slope())
|
||||
before = _files(tmp_path)
|
||||
stations, crosses = sections.sample_structure_sections(
|
||||
tmp_path, ROUTE, "csf", "tin", False, extra_stations=((130.0, ""),)
|
||||
tmp_path, ROUTE, "ptd", "implicit", False, extra_stations=((130.0, ""),)
|
||||
)
|
||||
assert _files(tmp_path) == before # 파일 · 폴더 하나도 안 생김
|
||||
assert [c["chainage_m"] for c in crosses] == [130.0] and len(stations) == 1
|
||||
sections.generate_irregular_sections(
|
||||
tmp_path, ROUTE, "csf", "tin", False, extra_stations=((130.0, "파형강관 D800"),)
|
||||
tmp_path, ROUTE, "ptd", "implicit", False, extra_stations=((130.0, "파형강관 D800"),)
|
||||
)
|
||||
written = json.loads(
|
||||
(tmp_path / "B06_Section/cross_sections/cross_00130m.json").read_text(encoding="utf-8")
|
||||
|
||||
@@ -235,7 +235,7 @@ def test_같은_입력_두_번이면_Node_0_쓰기_0(node, monkeypatch, tmp_path
|
||||
return copy.deepcopy(stored)
|
||||
if fn is prebuild.get_project_storage_relative_path:
|
||||
return "p"
|
||||
return {"source_filter": "csf", "method": "dtm", "smooth": False}
|
||||
return {"source_filter": "ptd", "method": "dtm", "smooth": False}
|
||||
|
||||
async def write_haul(connection, *, route_id, data_patch):
|
||||
written_haul.append(data_patch)
|
||||
|
||||
@@ -154,8 +154,8 @@ class _NoXyz(dict):
|
||||
def test_저장본이_맞으면_점군을_풀지_않음() -> None:
|
||||
source = _NoXyz(classification=np.zeros(3, np.uint8), bounds=np.zeros((3, 2)))
|
||||
data = _LazyStructured(source, _LazyXyz(source, 3))
|
||||
summary = summarize_masks(data, {"csf": np.array([True, False, True])})
|
||||
assert summary["csf"]["total_point_count"] == 3
|
||||
summary = summarize_masks(data, {"ptd": np.array([True, False, True])})
|
||||
assert summary["ptd"]["total_point_count"] == 3
|
||||
assert "classification" in data and "rgb" not in data
|
||||
assert len(data["classification"]) == 3
|
||||
|
||||
|
||||
@@ -54,30 +54,30 @@ def sheet_project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict:
|
||||
encoding="utf-8",
|
||||
)
|
||||
calls: list[str] = []
|
||||
real = SHEET_METHOD_BUILDERS["tin_sheet"]
|
||||
real = SHEET_METHOD_BUILDERS["laplace"]
|
||||
|
||||
def counting(*args, **kwargs): # type: ignore[no-untyped-def]
|
||||
calls.append("tin_sheet")
|
||||
calls.append("laplace")
|
||||
return real(*args, **kwargs)
|
||||
|
||||
monkeypatch.setitem(SHEET_METHOD_BUILDERS, "tin_sheet", counting)
|
||||
monkeypatch.setitem(SHEET_METHOD_BUILDERS, "laplace", counting)
|
||||
return {"root": tmp_path, "processed": processed, "models": models, "calls": calls}
|
||||
|
||||
|
||||
def _build(project: dict, route: np.ndarray = ROUTE) -> list[dict]:
|
||||
return sheet.build_sheet_surface_model(
|
||||
project["root"], project["processed"], project["models"], route, EPSG, ["tin_sheet"]
|
||||
project["root"], project["processed"], project["models"], route, EPSG, ["laplace"]
|
||||
)
|
||||
|
||||
|
||||
def test_도엽_3D_면_같은_입력_두번째는_안_셈(sheet_project: dict) -> None:
|
||||
first = _build(sheet_project)
|
||||
assert sheet_project["calls"] == ["tin_sheet"]
|
||||
assert (sheet_project["models"] / "dtm_sheet_tin_sheet.key.json").is_file()
|
||||
assert sheet_project["calls"] == ["laplace"]
|
||||
assert (sheet_project["models"] / "dtm_sheet_laplace.key.json").is_file()
|
||||
started = time.perf_counter()
|
||||
second = _build(sheet_project)
|
||||
assert time.perf_counter() - started < 0.5
|
||||
assert sheet_project["calls"] == ["tin_sheet"]
|
||||
assert sheet_project["calls"] == ["laplace"]
|
||||
assert second == first
|
||||
assert first[0]["generation_params"]["file_stamp"]
|
||||
|
||||
@@ -97,11 +97,11 @@ def test_도엽_3D_면_열쇠가_바뀌면_다시_셈(
|
||||
elif change == "algo":
|
||||
monkeypatch.setattr(sheet, "SHEET_SURFACE_ALGO", sheet.SHEET_SURFACE_ALGO + 1)
|
||||
elif change == "key_gone":
|
||||
(sheet_project["models"] / "dtm_sheet_tin_sheet.key.json").unlink()
|
||||
(sheet_project["models"] / "dtm_sheet_laplace.key.json").unlink()
|
||||
else:
|
||||
(sheet_project["models"] / "dtm_sheet_tin_sheet_preview.glb").unlink()
|
||||
(sheet_project["models"] / "dtm_sheet_laplace_preview.glb").unlink()
|
||||
_build(sheet_project, route)
|
||||
assert sheet_project["calls"] == ["tin_sheet", "tin_sheet"]
|
||||
assert sheet_project["calls"] == ["laplace", "laplace"]
|
||||
|
||||
|
||||
# ── 지표면 모델 id 유지(R2) ─────────────────────────────────────────────────────
|
||||
@@ -173,12 +173,12 @@ def _model(path: str, stamp: str) -> dict:
|
||||
"model_type": "dtm",
|
||||
"model_file_path": path,
|
||||
"resolution_m": 1.0,
|
||||
"generation_params": {"source_filter": "csf", "file_stamp": stamp},
|
||||
"generation_params": {"source_filter": "ptd", "file_stamp": stamp},
|
||||
"layers": [
|
||||
{
|
||||
"layer_name": "dtm_csf_preview",
|
||||
"layer_name": "dtm_ptd_preview",
|
||||
"geometry_type": "MESH",
|
||||
"file_path": "B04_PreProcess/models/dtm_csf_preview.glb",
|
||||
"file_path": "B04_PreProcess/models/dtm_ptd_preview.glb",
|
||||
"file_format": "glb",
|
||||
}
|
||||
],
|
||||
@@ -212,7 +212,7 @@ def _save(db: _FakeDb, models: list[dict]) -> list[int]:
|
||||
},
|
||||
"models": models,
|
||||
},
|
||||
source_filters=["csf"],
|
||||
source_filters=["ptd"],
|
||||
)
|
||||
)
|
||||
finally:
|
||||
@@ -221,8 +221,10 @@ def _save(db: _FakeDb, models: list[dict]) -> list[int]:
|
||||
|
||||
def test_파일_지문이_같으면_모델_id_유지() -> None:
|
||||
db = _FakeDb()
|
||||
path = "B04_PreProcess/models/dtm_csf.npz"
|
||||
first = _save(db, [_model(path, "1:2"), _model("B04_PreProcess/models/tin_csf.npz", "3:4")])
|
||||
path = "B04_PreProcess/models/dtm_ptd.npz"
|
||||
first = _save(
|
||||
db, [_model(path, "1:2"), _model("B04_PreProcess/models/implicit_ptd.npz", "3:4")]
|
||||
)
|
||||
second = _save(db, [_model(path, "1:2")])
|
||||
assert second == [first[0]]
|
||||
# 이번 결과에 없는 옛 행은 지움 · 같은 파일 행은 하나
|
||||
@@ -235,8 +237,8 @@ def test_도엽_한_방식_요청도_id_유지() -> None:
|
||||
from B04_PreProcess import B04_PreProcess_Repository as repo
|
||||
|
||||
db = _FakeDb()
|
||||
path = "B04_PreProcess/models/dtm_sheet_tin_sheet.npz"
|
||||
entry = {**_model(path, "5:6"), "generation_params": {"source_filter": "sheet_tin_sheet"}}
|
||||
path = "B04_PreProcess/models/dtm_sheet_laplace.npz"
|
||||
entry = {**_model(path, "5:6"), "generation_params": {"source_filter": "sheet_laplace"}}
|
||||
entry["generation_params"]["file_stamp"] = "5:6"
|
||||
first = asyncio.run(repo.save_sheet_surface_models(db, "p", [entry])) # type: ignore[arg-type]
|
||||
second = asyncio.run(repo.save_sheet_surface_models(db, "p", [entry])) # type: ignore[arg-type]
|
||||
@@ -265,7 +267,7 @@ def test_비용면_계류_메모리_기억(tmp_path: Path, monkeypatch: pytest.M
|
||||
monkeypatch.setattr(solver, "_cost_surface_signature", lambda *_a: signature["value"])
|
||||
monkeypatch.setattr(watershed, "_load_features", lambda *_a: [])
|
||||
monkeypatch.setattr(crs, "resolve_project_crs", lambda _root: "EPSG:5186")
|
||||
selection = {"source_filter": "csf", "method": "dtm", "smooth": False}
|
||||
selection = {"source_filter": "ptd", "method": "dtm", "smooth": False}
|
||||
terrain, _ = engine.load_terrain_and_streams(tmp_path, selection)
|
||||
again, _ = engine.load_terrain_and_streams(tmp_path, selection)
|
||||
assert loads == [1] and again is terrain and terrain.signature
|
||||
|
||||
@@ -62,14 +62,14 @@ def test_고정_표본은_JSON_길과_같은_점(tmp_path: Path, monkeypatch: py
|
||||
|
||||
|
||||
def test_원천이_그대로면_다시_안_만듦(tmp_path: Path) -> None:
|
||||
source = tmp_path / "ground_points_csf.npz"
|
||||
source = tmp_path / "ground_points_ptd.npz"
|
||||
_npz(source, 30, rgb=False)
|
||||
target = preview.ensure_point_preview(source, "csf")
|
||||
assert target.name == "preview_points_csf.bin"
|
||||
target = preview.ensure_point_preview(source, "ptd")
|
||||
assert target.name == "preview_points_ptd.bin"
|
||||
stamp = target.stat().st_mtime_ns
|
||||
assert preview.ensure_point_preview(source, "csf").stat().st_mtime_ns == stamp
|
||||
assert preview.ensure_point_preview(source, "ptd").stat().st_mtime_ns == stamp
|
||||
_npz(source, 40, rgb=False)
|
||||
head, _, rgb = _decode(preview.ensure_point_preview(source, "csf"))
|
||||
head, _, rgb = _decode(preview.ensure_point_preview(source, "ptd"))
|
||||
assert head["sampled_count"] == 40 and rgb is None
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from B05_Profile import B05_Profile_Router_Terrain as terrain_router
|
||||
from common_util.common_util_surface_sampler import dtm_grid_bytes
|
||||
|
||||
PID = "1bb0f876-983f-4ca5-9cc7-f59ee1f77e0a"
|
||||
PARAMS = {"source_filter": "csf", "method": "dtm", "smooth": True, "contour_interval_m": 1.0}
|
||||
PARAMS = {"source_filter": "ptd", "method": "dtm", "smooth": True, "contour_interval_m": 1.0}
|
||||
|
||||
|
||||
def _dtm(path: Path, z: float) -> None:
|
||||
@@ -35,11 +35,11 @@ def _dtm(path: Path, z: float) -> None:
|
||||
def _project(tmp_path: Path) -> Path:
|
||||
models = tmp_path / "B04_PreProcess" / "models"
|
||||
models.mkdir(parents=True)
|
||||
_dtm(models / "dtm_csf.npz", 100.0)
|
||||
_dtm(models / "dtm_csf_smooth.npz", 101.0)
|
||||
_dtm(models / "dtm_ptd.npz", 100.0)
|
||||
_dtm(models / "dtm_ptd_smooth.npz", 101.0)
|
||||
manifest = {
|
||||
"config_signature": "cfg1",
|
||||
"source_filters": {"csf": {"methods": {"dtm": {"smooth": {"smoothing_signature": "sm1"}}}}},
|
||||
"source_filters": {"ptd": {"methods": {"dtm": {"smooth": {"smoothing_signature": "sm1"}}}}},
|
||||
}
|
||||
(models / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
||||
return tmp_path
|
||||
@@ -83,19 +83,19 @@ def _grid(smooth: bool, etag: str | None = None):
|
||||
def test_평활_격자를_저장본으로_내고_같은_ETag_면_304(tmp_path, monkeypatch):
|
||||
root = _project(tmp_path)
|
||||
models = root / "B04_PreProcess" / "models"
|
||||
_patch(monkeypatch, root, ("dtm", "B04_PreProcess/models/dtm_csf.npz"))
|
||||
_patch(monkeypatch, root, ("dtm", "B04_PreProcess/models/dtm_ptd.npz"))
|
||||
response = _grid(True)
|
||||
assert response.status_code == 200
|
||||
saved = models / "dtm_csf_smooth_grid.bin"
|
||||
saved = models / "dtm_ptd_smooth_grid.bin"
|
||||
assert Path(response.path) == saved
|
||||
assert saved.read_bytes() == dtm_grid_bytes(models / "dtm_csf_smooth.npz")
|
||||
assert saved.read_bytes() == dtm_grid_bytes(models / "dtm_ptd_smooth.npz")
|
||||
assert _grid(True, response.headers["etag"]).status_code == 304
|
||||
assert Path(_grid(False).path).name == "dtm_csf_grid.bin"
|
||||
assert Path(_grid(False).path).name == "dtm_ptd_grid.bin"
|
||||
|
||||
|
||||
def test_DTM_아닌_표현은_404(tmp_path, monkeypatch):
|
||||
root = _project(tmp_path)
|
||||
_patch(monkeypatch, root, ("tin", "B04_PreProcess/models/tin_csf.npz"))
|
||||
_patch(monkeypatch, root, ("implicit", "B04_PreProcess/models/implicit_ptd.npz"))
|
||||
assert _grid(True).status_code == 404
|
||||
_patch(monkeypatch, root, None)
|
||||
assert _grid(True).status_code == 404
|
||||
@@ -103,16 +103,19 @@ def test_DTM_아닌_표현은_404(tmp_path, monkeypatch):
|
||||
|
||||
def test_받을_자리_서명은_manifest_구성과_평활(tmp_path):
|
||||
models = _project(tmp_path) / "B04_PreProcess" / "models"
|
||||
confirmed = (7, "dtm", "B04_PreProcess/models/dtm_csf.npz")
|
||||
confirmed = (7, "dtm", "B04_PreProcess/models/dtm_ptd.npz")
|
||||
grid = terrain_router._design_grid(PARAMS, models, confirmed)
|
||||
assert grid == {"model_id": 7, "smooth": True, "signature": "cfg1.sm1"}
|
||||
raw = terrain_router._design_grid({**PARAMS, "smooth": False}, models, confirmed)
|
||||
assert raw["signature"] == "cfg1."
|
||||
# 서버 표본기와 다른 파일 · DTM 아닌 표현 · 확정 없음 = 격자 안 냄(서버 표본 길)
|
||||
assert (
|
||||
terrain_router._design_grid({**PARAMS, "source_filter": "pmf"}, models, confirmed) is None
|
||||
terrain_router._design_grid(
|
||||
{**PARAMS, "source_filter": "classification"}, models, confirmed
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert terrain_router._design_grid({**PARAMS, "method": "tin"}, models, confirmed) is None
|
||||
assert terrain_router._design_grid({**PARAMS, "method": "implicit"}, models, confirmed) is None
|
||||
assert terrain_router._design_grid(PARAMS, models, None) is None
|
||||
|
||||
|
||||
@@ -122,7 +125,7 @@ def test_지반고_자릿수_기본_3_묶음은_6(tmp_path, monkeypatch):
|
||||
models.mkdir(parents=True)
|
||||
x = np.arange(0.0, 11.0)
|
||||
np.savez(
|
||||
models / "dtm_csf_smooth.npz",
|
||||
models / "dtm_ptd_smooth.npz",
|
||||
x=x,
|
||||
y=x,
|
||||
z=np.add.outer(x * 0.0, x * 0.1234567),
|
||||
@@ -144,10 +147,10 @@ def test_지반고_자릿수_기본_3_묶음은_6(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def test_격자_모델은_params_파일_확정_행이_어긋나도(monkeypatch):
|
||||
"""578-1 — 확정 행(sheet)이 params(csf)와 어긋나도 서버 표본기와 같은 `dtm_{filter}.npz` 행을 고름."""
|
||||
"""578-1 — 확정 행(sheet)이 params(ptd)와 어긋나도 서버 표본기와 같은 `dtm_{filter}.npz` 행을 고름."""
|
||||
rows = [
|
||||
(9, "dtm", "B04_PreProcess/models/dtm_sheet_laplace.npz"), # 확정(어긋남)
|
||||
(7, "dtm", "B04_PreProcess/models/dtm_csf.npz"),
|
||||
(7, "dtm", "B04_PreProcess/models/dtm_ptd.npz"),
|
||||
]
|
||||
|
||||
class _Cursor:
|
||||
@@ -163,6 +166,6 @@ def test_격자_모델은_params_파일_확정_행이_어긋나도(monkeypatch):
|
||||
yield _Cursor()
|
||||
|
||||
pick = asyncio.run(terrain_router._design_model(_Connection(), PID, PARAMS))
|
||||
assert pick == (7, "dtm", "B04_PreProcess/models/dtm_csf.npz")
|
||||
other = {**PARAMS, "source_filter": "pmf"}
|
||||
assert pick == (7, "dtm", "B04_PreProcess/models/dtm_ptd.npz")
|
||||
other = {**PARAMS, "source_filter": "classification"}
|
||||
assert asyncio.run(terrain_router._design_model(_Connection(), PID, other)) is None
|
||||
|
||||
@@ -331,7 +331,7 @@ def chain_world(monkeypatch, tmp_path):
|
||||
chain,
|
||||
"redesign_solve_request",
|
||||
lambda *a: b05_router.RouteSolveRequest(
|
||||
filter_key="csf",
|
||||
filter_key="ptd",
|
||||
method="dtm",
|
||||
smooth=False,
|
||||
algorithm="as_planned",
|
||||
|
||||
@@ -125,7 +125,7 @@ def _source_project(root: Path) -> None:
|
||||
|
||||
def test_원본에서_다시_읽기(tmp_path: Path) -> None:
|
||||
_source_project(tmp_path)
|
||||
surface = {"source_filter": "csf", "method": "dtm", "smooth": False}
|
||||
surface = {"source_filter": "ptd", "method": "dtm", "smooth": False}
|
||||
stored = load_design_route(tmp_path, surface, (20.0, 80.0))
|
||||
assert [v.y for v in stored.vertices] == [5.0, 5.0] # 정본이 먼저
|
||||
fresh = load_design_route(tmp_path, surface, (20.0, 80.0), from_source=True)
|
||||
@@ -175,7 +175,7 @@ def test_사용_범위_바꾸면_예상노선_다시_쓰고_미리_셈(
|
||||
return "x"
|
||||
|
||||
async def surface_params(*_):
|
||||
return {"source_filter": "csf", "method": "dtm", "smooth": False}
|
||||
return {"source_filter": "ptd", "method": "dtm", "smooth": False}
|
||||
|
||||
async def precompute(_pid):
|
||||
calls.append("precompute")
|
||||
|
||||
@@ -122,8 +122,8 @@ def hill_runs() -> dict:
|
||||
|
||||
@pytest.mark.parametrize("mode", ["follow", "gentle", "earthwork", "weighted"])
|
||||
def test_recommend_result_on_stations(hill_runs: dict, mode: str) -> None:
|
||||
"""추적(평면계획선 길)만 측점 맞춤 · 추천 갈래는 계산 로직 결과 그대로(769-1 · 요구 611)."""
|
||||
result = hill_runs[mode]
|
||||
assert result["off_station_curves"] == 0
|
||||
outline = build_planned_polyline(
|
||||
[tuple(n[k] for k in ("x", "y")) for n in result["nodes"]],
|
||||
min_radius_m=12.0,
|
||||
@@ -133,16 +133,18 @@ def test_recommend_result_on_stations(hill_runs: dict, mode: str) -> None:
|
||||
)
|
||||
# 화면이 펴서 다시 짠 선 = 서버 선
|
||||
assert np.allclose(outline.vertices, result["planned"], atol=1e-3)
|
||||
assert station_hits(outline) == len(outline.curves) > 0
|
||||
if mode != "follow": # 갈래는 기울기 상한 안(언덕은 비켜 갈 수 있음)
|
||||
if mode == "follow":
|
||||
assert result["off_station_curves"] == 0
|
||||
assert station_hits(outline) == len(outline.curves) > 0
|
||||
# 갈래는 기울기 상한 안(언덕은 비켜 갈 수 있음) · 비중은 제약을 벌점으로만(넘으면 목록)
|
||||
if mode in ("gentle", "earthwork"):
|
||||
assert result["metrics"]["over_limit"]["total"] == 0
|
||||
assert result["metrics"]["max_grade_pct"] <= 14.0 + 1e-6
|
||||
# 측점 맞춤 필수 — 맞추기 전 선은 안 냄(419)
|
||||
assert "unsnapped" not in result
|
||||
|
||||
|
||||
def test_합성_지형_modes_before_after(hill_runs: dict) -> None:
|
||||
"""합성 언덕(14 % · R12) — 갈래별 최대 기울기와 측점 밖 곡선을 함께 지킨다."""
|
||||
"""합성 언덕(14 % · R12) — 추적만 측점 위 · 추천 갈래는 측점 맞춤 없음(769-1)."""
|
||||
on_total, curves_total = 0, 0
|
||||
for mode, result in hill_runs.items():
|
||||
after = result["metrics"]
|
||||
@@ -155,7 +157,8 @@ def test_합성_지형_modes_before_after(hill_runs: dict) -> None:
|
||||
f" · 곡선 {curves}(측점 밖 {result['off_station_curves']})"
|
||||
)
|
||||
print(f"측점 위 곡선 {on_total}/{curves_total} = {100 * on_total / curves_total:.0f}%")
|
||||
assert curves_total and on_total == curves_total
|
||||
assert curves_total and hill_runs["follow"]["off_station_curves"] == 0
|
||||
assert on_total < curves_total # 추천 갈래는 맞추지 않음
|
||||
|
||||
|
||||
def test_합성_예상노선_eight_outlines() -> None:
|
||||
|
||||
@@ -203,13 +203,12 @@ def hill_runs() -> dict:
|
||||
|
||||
@pytest.mark.parametrize("mode", ["gradeline", "earthwork", "gentle", "weighted"])
|
||||
def test_hill_branches_rules(hill_runs: dict, mode: str) -> None:
|
||||
"""언덕 — 꺾임만 0 · 측점 밖 0 · 못 지킨 곳 수 = R · 계류 · 측점 · 맞추기 전 선 없음."""
|
||||
"""언덕 — 꺾임만 0 · 못 지킨 곳 수 = R · 계류 · 맞추기 전 선 없음(측점 맞춤 없음 · 769-1)."""
|
||||
result = hill_runs[mode]
|
||||
interior = result["nodes"][1:-1]
|
||||
assert interior and all(node["radius_m"] is not None for node in interior) # 삼각 0
|
||||
assert result["off_station_curves"] == 0
|
||||
over = result["metrics"]["over_limit"]
|
||||
assert over["total"] == over["radius"] + over["stream"] + over.get("station", 0)
|
||||
assert over["total"] == over["radius"] + over["stream"]
|
||||
assert "unsnapped" not in result
|
||||
assert result["ends_over_limit"] is False and result["ends_grade_pct"] < 14.0
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ def test_E1_재설계_요청이_설정을_옮김():
|
||||
"enforce_pipe_clearance": True,
|
||||
"station_display": {"station": 12, "cumulative": 240},
|
||||
}
|
||||
request = redesign_solve_request(params, {"source_filter": "csf"}, 5)
|
||||
request = redesign_solve_request(params, {"source_filter": "ptd"}, 5)
|
||||
assert request.design_speed_kph == 30
|
||||
assert request.enforce_pipe_clearance is True
|
||||
assert request.station_display == {"station": 12, "cumulative": 240}
|
||||
|
||||
@@ -167,8 +167,8 @@ async def _aboom(*_a, **_k):
|
||||
|
||||
|
||||
_REQUEST = RouteConfirmRequest(
|
||||
filter_key="csf",
|
||||
method="tin",
|
||||
filter_key="ptd",
|
||||
method="implicit",
|
||||
surface_model_id=1,
|
||||
uphill_overrides=[{"chainage_m": 40.0, "side": "right"}],
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ from common_util.common_util_surface_sampler import build_surface_sampler, check
|
||||
from M02_MasterTemplete import M02_Item_Snapshot as snap
|
||||
|
||||
BAD_IDS = ["../../..", "..", "a/b", "a\\b", "C:/Windows", "/etc", ".hidden", "", "x" * 65]
|
||||
BAD_KEYS = ["../other/dtm_csf", "csf/../../x", "C:/x", "", "CSF"]
|
||||
BAD_KEYS = ["../other/dtm_ptd", "ptd/../../x", "C:/x", "", "PTD", "csf"]
|
||||
|
||||
|
||||
def _structure(structure_id):
|
||||
@@ -80,7 +80,7 @@ def test_H2_스키마_허용_목록(bad):
|
||||
with pytest.raises(ValidationError):
|
||||
RouteSolveRequest(filter_key=bad, **body)
|
||||
with pytest.raises(ValidationError):
|
||||
RouteSolveRequest(filter_key="csf", method=bad, **body)
|
||||
RouteSolveRequest(filter_key="ptd", method=bad, **body)
|
||||
with pytest.raises(ValidationError):
|
||||
RouteConfirmRequest(filter_key=bad)
|
||||
with pytest.raises(ValidationError):
|
||||
@@ -88,7 +88,7 @@ def test_H2_스키마_허용_목록(bad):
|
||||
|
||||
|
||||
def test_H2_스키마_정상값_통과():
|
||||
assert RouteConfirmRequest(filter_key="grid_min_z", method="tin").method == "tin"
|
||||
assert RouteConfirmRequest(filter_key="ptd_refine", method="implicit").method == "implicit"
|
||||
assert RouteConfirmRequest().filter_key is None
|
||||
|
||||
|
||||
@@ -97,13 +97,13 @@ def test_H2_파일_이름_만드는_곳(bad, tmp_path):
|
||||
with pytest.raises(ValueError):
|
||||
check_surface_keys(bad)
|
||||
with pytest.raises(ValueError):
|
||||
check_surface_keys("csf", bad)
|
||||
check_surface_keys("ptd", bad)
|
||||
with pytest.raises(ValueError):
|
||||
build_surface_sampler(tmp_path, bad, "dtm", False)
|
||||
with pytest.raises(ValueError):
|
||||
_load_dtm_grid(tmp_path, bad, False)
|
||||
with pytest.raises(ValueError):
|
||||
_source_npz_paths(tmp_path, "csf", bad, False)
|
||||
_source_npz_paths(tmp_path, "ptd", bad, False)
|
||||
with pytest.raises(ValueError):
|
||||
load_or_build_skeleton(tmp_path, bad, "dtm", False)
|
||||
check_surface_keys("classification", "meshfree")
|
||||
check_surface_keys("classification", "implicit")
|
||||
|
||||
@@ -58,7 +58,7 @@ class FakeWfs:
|
||||
|
||||
@pytest.fixture
|
||||
def world(tmp_path, monkeypatch):
|
||||
"""D 프로젝트(수치지형도) · B 프로젝트(라이다 csf 확정 + 수치지형도 서피스 · B04 지적도)."""
|
||||
"""D 프로젝트(수치지형도) · B 프로젝트(라이다 ptd 확정 + 수치지형도 서피스 · B04 지적도)."""
|
||||
import common_util.common_util_crs as crs
|
||||
import common_util.common_util_surface_confirmation as confirm
|
||||
import config.config_db as config_db
|
||||
@@ -66,8 +66,8 @@ def world(tmp_path, monkeypatch):
|
||||
d_root, b_root = tmp_path / "D", tmp_path / "B"
|
||||
_dem(d_root / "B04_PreProcess" / "models_plan" / "dtm_sheet_laplace.npz", 0.3)
|
||||
b_models = b_root / "B04_PreProcess" / "models"
|
||||
_dem(b_models / "dtm_csf.npz", 0.6)
|
||||
_dem(b_models / "dtm_csf_smooth.npz", 0.5)
|
||||
_dem(b_models / "dtm_ptd.npz", 0.6)
|
||||
_dem(b_models / "dtm_ptd_smooth.npz", 0.5)
|
||||
_dem(b_models / "dtm_sheet_laplace_smooth.npz", 0.2)
|
||||
processed = b_root / "B04_PreProcess" / "processed"
|
||||
processed.mkdir(parents=True)
|
||||
@@ -76,7 +76,7 @@ def world(tmp_path, monkeypatch):
|
||||
(processed / "vworld_white_meta.json").write_text(json.dumps(meta), "utf-8")
|
||||
kinds = {"D": "PLAN", "B": "ROAD"}
|
||||
state = {"pid": "D"}
|
||||
params = {"source_filter": "csf", "method": "dtm", "smooth": True}
|
||||
params = {"source_filter": "ptd", "method": "dtm", "smooth": True}
|
||||
|
||||
async def project_row(project_id):
|
||||
return {"project_kind": kinds[state["pid"]], "storage_path": state["pid"]}
|
||||
@@ -183,7 +183,7 @@ def test_lidar_zero_order_whole_grid(world) -> None:
|
||||
n, cell = 200, 1.0
|
||||
x, y = X0 + np.arange(n) * cell, Y0 + np.arange(n) * cell
|
||||
valley = np.abs(x - x.mean())[None, :] * 0.3 + (y - Y0)[:, None] * 0.2
|
||||
npz = world["B"] / "B04_PreProcess" / "models" / "dtm_csf_smooth.npz"
|
||||
npz = world["B"] / "B04_PreProcess" / "models" / "dtm_ptd_smooth.npz"
|
||||
np.savez(npz, x=x, y=y, z=valley, valid_mask=np.ones(valley.shape, bool), resolution=[cell])
|
||||
res = world["get"]("B", "zero-order", terrain="lidar")
|
||||
got = res.json()
|
||||
|
||||
@@ -183,23 +183,24 @@ def test_갈음_안_지은_필터_파일_지움(tmp_path: Path) -> None:
|
||||
"dtm_sheet_laplace.key.json",
|
||||
]
|
||||
stale = [
|
||||
"dtm_csf.npz",
|
||||
"dtm_csf_preview.glb",
|
||||
"dtm_csf_smooth.npz",
|
||||
"dtm_csf_smooth_grid.bin",
|
||||
"tin_csf.npz",
|
||||
"contour_csf_dtm_smooth_1.0m.json",
|
||||
"dtm_ptd.npz",
|
||||
"dtm_ptd_preview.glb",
|
||||
"dtm_ptd_smooth.npz",
|
||||
"dtm_ptd_smooth_grid.bin",
|
||||
"implicit_ptd.npz",
|
||||
"contour_ptd_dtm_smooth_1.0m.json",
|
||||
]
|
||||
for name in keep + stale:
|
||||
(models / name).write_bytes(b"x")
|
||||
for name in ["mask_csf.npy", "ground_points_csf.npz", "preview_points_csf.bin"]:
|
||||
for name in ["mask_ptd.npy", "ground_points_ptd.npz", "preview_points_ptd.bin"]:
|
||||
(processed / name).write_bytes(b"x")
|
||||
(processed / "mask_classification.npy").write_bytes(b"x")
|
||||
manifest = {"source_filters": {"classification": {}, "csf": {}}, "x": 1}
|
||||
manifest = {"source_filters": {"classification": {}, "ptd": {}}, "x": 1}
|
||||
(models / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
||||
gone = lidar_upload.drop_stale_filters(tmp_path, ["classification"])
|
||||
assert gone == sorted(
|
||||
f"B04_PreProcess/models/{n}" for n in ["dtm_csf.npz", "dtm_csf_smooth.npz", "tin_csf.npz"]
|
||||
f"B04_PreProcess/models/{n}"
|
||||
for n in ["dtm_ptd.npz", "dtm_ptd_smooth.npz", "implicit_ptd.npz"]
|
||||
)
|
||||
assert sorted(p.name for p in models.iterdir()) == sorted([*keep, "manifest.json"])
|
||||
assert [p.name for p in processed.iterdir()] == ["mask_classification.npy"]
|
||||
|
||||
@@ -52,31 +52,31 @@ def _band_contour(path: Path) -> bool:
|
||||
|
||||
def test_implicit_만_지어도_낡은_dtm_같이_다시(tmp_path: Path) -> None:
|
||||
data, mask = _banded_plane()
|
||||
masks = {"csf": mask}
|
||||
build_all_terrain_models(data, masks, tmp_path, _config("csf", ["dtm", "implicit"], 1.0))
|
||||
narrow = _valid_area(tmp_path / "dtm_csf.npz")
|
||||
contour = tmp_path / "contour_csf_implicit_1.0m.json"
|
||||
masks = {"ptd": mask}
|
||||
build_all_terrain_models(data, masks, tmp_path, _config("ptd", ["dtm", "implicit"], 1.0))
|
||||
narrow = _valid_area(tmp_path / "dtm_ptd.npz")
|
||||
contour = tmp_path / "contour_ptd_implicit_1.0m.json"
|
||||
assert not _band_contour(contour) # 1 m 닫기 = 띠 빈칸 · 106 m 줄 잘림
|
||||
|
||||
# 화면 [서피스 생성] = 고른 표현 하나만 — 외곽 설정(지문)이 바뀌었으니 dtm 도 다시
|
||||
manifest = build_all_terrain_models(data, masks, tmp_path, _config("csf", ["implicit"], 3.0))
|
||||
methods = manifest["source_filters"]["csf"]["methods"]
|
||||
manifest = build_all_terrain_models(data, masks, tmp_path, _config("ptd", ["implicit"], 3.0))
|
||||
methods = manifest["source_filters"]["ptd"]["methods"]
|
||||
assert methods["dtm"]["status"] == "completed"
|
||||
wide = _valid_area(tmp_path / "dtm_csf.npz")
|
||||
grid_area = _valid_area(implicit_grid_npz(tmp_path, "csf"))
|
||||
wide = _valid_area(tmp_path / "dtm_ptd.npz")
|
||||
grid_area = _valid_area(implicit_grid_npz(tmp_path, "ptd"))
|
||||
glb_area = methods["implicit"]["footprint_area_m2"]
|
||||
assert wide > 1.05 * narrow
|
||||
assert grid_area == wide # 격자 외곽 = dtm 외곽
|
||||
assert abs(glb_area - wide) / wide < 0.05 # 3D 외곽 = 격자 외곽
|
||||
assert _band_contour(contour) # 등고선이 넓힌 띠까지
|
||||
assert not contour_cache_is_stale(contour, tmp_path / "implicit_csf.npz")
|
||||
assert not contour_cache_is_stale(contour, tmp_path / "implicit_ptd.npz")
|
||||
|
||||
|
||||
def test_dtm_이_바뀌면_등고선_캐시_낡음(tmp_path: Path) -> None:
|
||||
model = tmp_path / "implicit_csf.npz"
|
||||
dtm = tmp_path / "dtm_csf.npz"
|
||||
grid = tmp_path / "implicit_csf_grid.npz"
|
||||
contour = tmp_path / "contour_csf_implicit_1.0m.json"
|
||||
model = tmp_path / "implicit_ptd.npz"
|
||||
dtm = tmp_path / "dtm_ptd.npz"
|
||||
grid = tmp_path / "implicit_ptd_grid.npz"
|
||||
contour = tmp_path / "contour_ptd_implicit_1.0m.json"
|
||||
assert contour_cache_is_stale(contour, model) # 없음
|
||||
for index, path in enumerate((model, dtm, grid, contour)):
|
||||
path.write_bytes(b"x")
|
||||
@@ -86,7 +86,7 @@ def test_dtm_이_바뀌면_등고선_캐시_낡음(tmp_path: Path) -> None:
|
||||
os.utime(newer, ns=(2 * 10**18, 2 * 10**18))
|
||||
assert contour_cache_is_stale(contour, model)
|
||||
os.utime(newer, ns=(10**18, 10**18))
|
||||
tin_smooth = tmp_path / "tin_csf_smooth.npz"
|
||||
tin_smooth = tmp_path / "tin_ptd_smooth.npz"
|
||||
tin_smooth.write_bytes(b"x")
|
||||
os.utime(tin_smooth, ns=(10**18, 10**18))
|
||||
os.utime(dtm, ns=(2 * 10**18, 2 * 10**18))
|
||||
|
||||
@@ -135,9 +135,10 @@ def test_implicit_파일_없으면_까닭_그대로(tmp_path) -> None:
|
||||
|
||||
def test_깨진_모델은_DTM_대체_없이_오류(tmp_path) -> None:
|
||||
models = _models(tmp_path)
|
||||
(models / "tin_classification.npz").write_bytes(b"broken")
|
||||
(models / "implicit_classification_grid.npz").unlink(missing_ok=True)
|
||||
(models / "implicit_classification.npz").write_bytes(b"broken")
|
||||
with pytest.raises(ValueError, match="지표면 모델을 읽지 못함"):
|
||||
solver._build_cost_surface(models, "classification", "tin", False)
|
||||
solver._build_cost_surface(models, "classification", "implicit", False)
|
||||
source = (Path(solver.__file__)).read_text(encoding="utf-8")
|
||||
assert "return dtm_z\n\n return dtm_z" not in source
|
||||
assert "except Exception:\n return dtm_z" not in source
|
||||
|
||||
@@ -128,7 +128,7 @@ def b04_world(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def test_분석은_보기만_단계_확정_그대로(b04_world) -> None:
|
||||
request = SurfaceAnalyzeRequest(input_file_id=3, source_filters=["csf"], methods=["implicit"])
|
||||
request = SurfaceAnalyzeRequest(input_file_id=3, source_filters=["ptd"], methods=["implicit"])
|
||||
answer = asyncio.run(b04.analyze_surface(uuid4(), request))
|
||||
assert answer.surface_model_ids == [1, 2]
|
||||
sql = " | ".join(b04_world["log"])
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
"""실데이터 등가성 — 래스터화가 기존 griddata 경로와 같은 표고를 내는지.
|
||||
|
||||
PLAN 2026-08-17 완료 조건 검증.
|
||||
- `meshfree`: Delaunay를 그대로 쓰므로 표고 완전 일치
|
||||
- `tin`: 저장 faces와 재Delaunay는 다른 삼각망이라 차이가 남는다. 그 차이가
|
||||
**보간 버그가 아니라 삼각망 선택 차이**임을 대조로 분리한다.
|
||||
|
||||
**주의: griddata가 호출당 3분 걸려 이 파일 전체 실행에 약 8분이 든다.**
|
||||
실제 프로젝트 산출물이 없으면 skip.
|
||||
"""
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from scipy.interpolate import griddata
|
||||
from scipy.spatial import Delaunay
|
||||
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Contour import (
|
||||
_grid_axes,
|
||||
extract_contours,
|
||||
rasterize_triangle_mesh,
|
||||
)
|
||||
|
||||
MODELS = Path(
|
||||
"C:/Program_coding/임도설계 및 견적자동화 프로그램 개발/storage/1/3/"
|
||||
"f9543035-4a91-4df0-b467-cdc515723507/B04_PreProcess/models"
|
||||
)
|
||||
|
||||
|
||||
def _load(name):
|
||||
path = MODELS / name
|
||||
if not path.exists():
|
||||
pytest.skip(f"실데이터 없음: {path}")
|
||||
return np.load(path)
|
||||
|
||||
|
||||
def _axes_for(xy):
|
||||
return _grid_axes(
|
||||
float(np.min(xy[:, 0])),
|
||||
float(np.max(xy[:, 0])),
|
||||
float(np.min(xy[:, 1])),
|
||||
float(np.max(xy[:, 1])),
|
||||
1.0,
|
||||
)
|
||||
|
||||
|
||||
def _compare(old, new):
|
||||
return {
|
||||
"only_old": int((np.isfinite(old) & ~np.isfinite(new)).sum()),
|
||||
"only_new": int((np.isfinite(new) & ~np.isfinite(old)).sum()),
|
||||
"both": np.isfinite(old) & np.isfinite(new),
|
||||
}
|
||||
|
||||
|
||||
def test_meshfree_matches_griddata_exactly():
|
||||
"""meshfree — 같은 Delaunay를 쓰므로 표고가 완전히 일치해야 한다."""
|
||||
points = _load("meshfree_csf.npz")["points"]
|
||||
x_coords, y_coords = _axes_for(points)
|
||||
xx, yy = np.meshgrid(x_coords, y_coords)
|
||||
|
||||
triangulation = Delaunay(np.asarray(points[:, :2], dtype=np.float64))
|
||||
new = rasterize_triangle_mesh(points, triangulation.simplices, x_coords, y_coords)
|
||||
old = griddata(points[:, :2], points[:, 2], (xx, yy), method="linear")
|
||||
|
||||
result = _compare(old, new)
|
||||
assert result["both"].sum() > 10_000, "비교 대상 셀이 너무 적다"
|
||||
assert result["only_old"] == 0, "기존이 채우던 셀이 비었다"
|
||||
assert result["only_new"] == 0, "없던 셀이 생겼다"
|
||||
assert np.abs(old[result["both"]] - new[result["both"]]).max() < 1e-9
|
||||
|
||||
|
||||
def test_rasterize_matches_griddata_on_same_triangulation():
|
||||
"""래스터화 정확성 — 같은 재Delaunay를 넣으면 griddata와 완전 일치.
|
||||
|
||||
이 대조가 통과하면 tin에서 남는 차이는 전부 삼각망 선택 차이다.
|
||||
"""
|
||||
vertices = _load("tin_csf.npz")["vertices"]
|
||||
x_coords, y_coords = _axes_for(vertices)
|
||||
xx, yy = np.meshgrid(x_coords, y_coords)
|
||||
|
||||
triangulation = Delaunay(np.asarray(vertices[:, :2], dtype=np.float64))
|
||||
control = rasterize_triangle_mesh(vertices, triangulation.simplices, x_coords, y_coords)
|
||||
old = griddata(vertices[:, :2], vertices[:, 2], (xx, yy), method="linear")
|
||||
|
||||
result = _compare(old, control)
|
||||
assert result["only_old"] == 0 and result["only_new"] == 0
|
||||
assert np.abs(old[result["both"]] - control[result["both"]]).max() < 1e-9
|
||||
|
||||
|
||||
def test_tin_difference_comes_only_from_triangulation():
|
||||
"""tin 저장 faces — griddata 대비 차이가 재Delaunay 대비 차이와 같아야 한다.
|
||||
|
||||
두 대조가 일치하면 차이의 원인은 삼각망 하나뿐이다. 저장 faces는 긴 변 제거·
|
||||
외곽 클리핑이 반영된 실제 TIN이므로(`ModelBuild.py:38-51`) 재Delaunay보다 덮는
|
||||
면적이 작고, 그 몫이 `only_old`로 나온다.
|
||||
"""
|
||||
data = _load("tin_csf.npz")
|
||||
vertices, faces = data["vertices"], data["faces"]
|
||||
x_coords, y_coords = _axes_for(vertices)
|
||||
xx, yy = np.meshgrid(x_coords, y_coords)
|
||||
|
||||
stored = rasterize_triangle_mesh(vertices, faces, x_coords, y_coords)
|
||||
triangulation = Delaunay(np.asarray(vertices[:, :2], dtype=np.float64))
|
||||
control = rasterize_triangle_mesh(vertices, triangulation.simplices, x_coords, y_coords)
|
||||
old = griddata(vertices[:, :2], vertices[:, 2], (xx, yy), method="linear")
|
||||
|
||||
vs_griddata = _compare(old, stored)
|
||||
vs_control = _compare(control, stored)
|
||||
assert vs_griddata["only_old"] == vs_control["only_old"]
|
||||
assert vs_griddata["only_new"] == vs_control["only_new"] == 0
|
||||
|
||||
diff = np.abs(old[vs_griddata["both"]] - stored[vs_griddata["both"]])
|
||||
changed = diff > 1e-6
|
||||
print(
|
||||
f"\n[tin] 공통셀={int(vs_griddata['both'].sum()):,} "
|
||||
f"저장 faces가 안 덮는 셀={vs_griddata['only_old']:,}\n"
|
||||
f" 값 다른 셀={int(changed.sum()):,} ({changed.mean():.2%}) "
|
||||
f"최대={diff.max():.4f}m 차이셀 평균={diff[changed].mean():.4f}m"
|
||||
)
|
||||
# 대각선 선택이 다른 사각형에서만 어긋나므로 국소 지형 기복을 넘지 않아야 한다.
|
||||
assert diff.max() < 1.0, "표고차 1m 초과 — 삼각망 차이로 설명 불가"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"npz_name,representation,cached_name,exact",
|
||||
[
|
||||
("tin_csf.npz", "triangular_mesh", "contour_csf_tin_1.0m.json", False),
|
||||
("meshfree_csf.npz", "meshfree_surfels", "contour_csf_meshfree_1.0m.json", True),
|
||||
("dtm_csf.npz", "regular_grid", "contour_csf_dtm_1.0m.json", True),
|
||||
],
|
||||
)
|
||||
def test_contour_output_matches_cached(npz_name, representation, cached_name, exact):
|
||||
"""최종 산출물 대조 — 캐시된 등고선 JSON과 레벨·정점 수를 맞춰 본다."""
|
||||
import json
|
||||
|
||||
npz_path = MODELS / npz_name
|
||||
cached_path = MODELS / cached_name
|
||||
if not npz_path.exists() or not cached_path.exists():
|
||||
pytest.skip("실데이터 없음")
|
||||
|
||||
started = time.perf_counter()
|
||||
lines = extract_contours(npz_path, representation, 1.0)
|
||||
elapsed = time.perf_counter() - started
|
||||
with open(cached_path, encoding="utf-8") as handle:
|
||||
cached = json.load(handle)["contours"]
|
||||
|
||||
new_levels = {round(float(item["level"]), 3) for item in lines}
|
||||
old_levels = {round(float(item["level"]), 3) for item in cached}
|
||||
new_points = sum(len(item["coordinates"]) for item in lines)
|
||||
old_points = sum(len(item["coordinates"]) for item in cached)
|
||||
print(
|
||||
f"\n[{npz_name}] {elapsed:.2f}초 세그먼트 {len(lines):,}/{len(cached):,} "
|
||||
f"정점 {new_points:,}/{old_points:,}"
|
||||
)
|
||||
|
||||
assert new_levels == old_levels, "등고선 레벨 집합이 바뀌었다"
|
||||
if exact:
|
||||
assert len(lines) == len(cached)
|
||||
assert new_points == old_points
|
||||
else:
|
||||
# tin은 삼각망 차이만큼 경계 세그먼트가 미세하게 달라진다.
|
||||
assert abs(new_points - old_points) / old_points < 0.01
|
||||
|
||||
|
||||
def test_contour_extraction_is_fast():
|
||||
"""가속이 목적이므로 속도를 회귀로 못박는다 (변경 전 tin 119초·meshfree 302초)."""
|
||||
for npz_name, representation in (
|
||||
("tin_csf.npz", "triangular_mesh"),
|
||||
("meshfree_csf.npz", "meshfree_surfels"),
|
||||
):
|
||||
path = MODELS / npz_name
|
||||
if not path.exists():
|
||||
pytest.skip("실데이터 없음")
|
||||
started = time.perf_counter()
|
||||
extract_contours(path, representation, 1.0)
|
||||
elapsed = time.perf_counter() - started
|
||||
print(f"\n[속도] {npz_name} {elapsed:.2f}초")
|
||||
assert elapsed < 30.0, f"{npz_name} {elapsed:.1f}초 — 가속 실패"
|
||||
@@ -1,195 +0,0 @@
|
||||
"""B04 삼각망 래스터화 — 위치탐색 없이 격자 표고를 만드는 함수의 정확도 검증.
|
||||
|
||||
기존 경로는 scipy `griddata`가 질의점마다 삼각형 98만개 망을 탐색해 등고선 1회에
|
||||
2~5분이 걸렸다(PLAN 2026-08-17). 삼각형을 격자에 직접 래스터화하면 탐색이 사라진다.
|
||||
정밀도 유지가 조건이므로 무게중심 선형보간의 해석해 일치를 먼저 못박는다.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Contour import rasterize_triangle_mesh
|
||||
|
||||
|
||||
def _axes(x_min, x_max, y_min, y_max, step=1.0):
|
||||
cols = int(round((x_max - x_min) / step)) + 1
|
||||
rows = int(round((y_max - y_min) / step)) + 1
|
||||
return (
|
||||
np.linspace(x_min, x_max, cols, dtype=np.float64),
|
||||
np.linspace(y_min, y_max, rows, dtype=np.float64),
|
||||
)
|
||||
|
||||
|
||||
def _plane(x, y):
|
||||
"""검증용 기준 평면 — 선형보간이 정확하면 오차 0이어야 한다."""
|
||||
return 2.0 * x + 3.0 * y + 5.0
|
||||
|
||||
|
||||
def test_single_triangle_matches_plane_solution():
|
||||
"""평면 위 삼각형 하나 — 내부 격자점이 해석해와 일치."""
|
||||
verts = np.array(
|
||||
[
|
||||
[0.0, 0.0, _plane(0.0, 0.0)],
|
||||
[10.0, 0.0, _plane(10.0, 0.0)],
|
||||
[0.0, 10.0, _plane(0.0, 10.0)],
|
||||
]
|
||||
)
|
||||
tris = np.array([[0, 1, 2]])
|
||||
x_coords, y_coords = _axes(0.0, 10.0, 0.0, 10.0)
|
||||
|
||||
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
|
||||
|
||||
xx, yy = np.meshgrid(x_coords, y_coords)
|
||||
filled = np.isfinite(z_grid)
|
||||
assert filled.any()
|
||||
assert np.allclose(z_grid[filled], _plane(xx[filled], yy[filled]), atol=1e-9)
|
||||
|
||||
|
||||
def test_cells_outside_mesh_stay_nan():
|
||||
"""삼각망 밖은 NaN — footprint 마스크가 기존과 같이 동작해야 한다."""
|
||||
verts = np.array([[0.0, 0.0, 1.0], [2.0, 0.0, 1.0], [0.0, 2.0, 1.0]])
|
||||
tris = np.array([[0, 1, 2]])
|
||||
x_coords, y_coords = _axes(0.0, 10.0, 0.0, 10.0)
|
||||
|
||||
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
|
||||
|
||||
# 빗변 바깥쪽 (9, 9)는 삼각형 밖
|
||||
assert np.isnan(z_grid[9, 9])
|
||||
# 꼭짓점 (0, 0)은 삼각형 안
|
||||
assert z_grid[0, 0] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_two_triangles_fill_full_square():
|
||||
"""사각형을 이루는 두 삼각형 — 격자 전체가 빈칸 없이 채워진다."""
|
||||
corners = [(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)]
|
||||
verts = np.array([[x, y, _plane(x, y)] for x, y in corners])
|
||||
tris = np.array([[0, 1, 2], [0, 2, 3]])
|
||||
x_coords, y_coords = _axes(0.0, 4.0, 0.0, 4.0)
|
||||
|
||||
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
|
||||
|
||||
assert np.isfinite(z_grid).all()
|
||||
xx, yy = np.meshgrid(x_coords, y_coords)
|
||||
assert np.allclose(z_grid, _plane(xx, yy), atol=1e-9)
|
||||
|
||||
|
||||
def test_shared_edge_is_consistent():
|
||||
"""두 삼각형이 공유하는 변 위의 격자점은 어느 쪽으로 계산해도 같은 값."""
|
||||
verts = np.array(
|
||||
[[0.0, 0.0, 0.0], [4.0, 0.0, 8.0], [4.0, 4.0, 20.0], [0.0, 4.0, 12.0]]
|
||||
) # z = 2x + 3y
|
||||
tris = np.array([[0, 1, 2], [0, 2, 3]])
|
||||
x_coords, y_coords = _axes(0.0, 4.0, 0.0, 4.0)
|
||||
|
||||
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
|
||||
|
||||
xx, yy = np.meshgrid(x_coords, y_coords)
|
||||
assert np.allclose(z_grid, 2.0 * xx + 3.0 * yy, atol=1e-9)
|
||||
|
||||
|
||||
def test_degenerate_triangle_is_skipped():
|
||||
"""면적 0 삼각형이 섞여 있어도 죽지 않고 나머지를 채운다."""
|
||||
verts = np.array(
|
||||
[
|
||||
[0.0, 0.0, 1.0],
|
||||
[4.0, 0.0, 1.0],
|
||||
[0.0, 4.0, 1.0],
|
||||
[1.0, 1.0, 99.0],
|
||||
[2.0, 2.0, 99.0],
|
||||
[3.0, 3.0, 99.0],
|
||||
]
|
||||
)
|
||||
tris = np.array([[0, 1, 2], [3, 4, 5]]) # 두 번째는 일직선
|
||||
x_coords, y_coords = _axes(0.0, 4.0, 0.0, 4.0)
|
||||
|
||||
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
|
||||
|
||||
filled = np.isfinite(z_grid)
|
||||
assert filled.any()
|
||||
assert np.allclose(z_grid[filled], 1.0)
|
||||
|
||||
|
||||
def test_triangle_smaller_than_cell_still_lands():
|
||||
"""격자 간격보다 작은 삼각형도 자기가 덮는 격자점을 채운다 (실데이터는 m²당 13.7면)."""
|
||||
verts = np.array([[1.6, 1.6, 7.0], [2.4, 1.6, 7.0], [2.0, 2.4, 7.0]])
|
||||
tris = np.array([[0, 1, 2]])
|
||||
x_coords, y_coords = _axes(0.0, 4.0, 0.0, 4.0)
|
||||
|
||||
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
|
||||
|
||||
assert z_grid[2, 2] == pytest.approx(7.0)
|
||||
assert np.isnan(z_grid[0, 0])
|
||||
|
||||
|
||||
def test_float32_utm_axis_does_not_drop_cells():
|
||||
"""float32 축(UTM 좌표) — 등간격 환산으로 인덱스를 내면 셀을 놓친다.
|
||||
|
||||
`_grid_axes`가 float32를 쓰는데 18만대 좌표에서 해상도가 0.015625m라 간격이
|
||||
0.984~1.0으로 흔들린다. 등간격 가정 시 최대 0.81셀 어긋나 실측 8,819셀이
|
||||
비었다(2026-08-17). 축을 직접 탐색해야 한다.
|
||||
"""
|
||||
x_coords = np.linspace(183433.6, 183805.8, 374, dtype=np.float32)
|
||||
y_coords = np.linspace(489168.8, 489570.2, 403, dtype=np.float32)
|
||||
|
||||
# 격자 전체를 덮는 큰 삼각형 2개 → 모든 셀이 채워져야 한다.
|
||||
x0, x1 = float(x_coords[0]) - 5.0, float(x_coords[-1]) + 5.0
|
||||
y0, y1 = float(y_coords[0]) - 5.0, float(y_coords[-1]) + 5.0
|
||||
corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
||||
verts = np.array([[x, y, _plane(x, y)] for x, y in corners])
|
||||
tris = np.array([[0, 1, 2], [0, 2, 3]])
|
||||
|
||||
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_coords)
|
||||
|
||||
assert np.isfinite(z_grid).all(), f"빈 셀 {int((~np.isfinite(z_grid)).sum()):,}개"
|
||||
xx, yy = np.meshgrid(np.asarray(x_coords, np.float64), np.asarray(y_coords, np.float64))
|
||||
assert np.abs(z_grid - _plane(xx, yy)).max() < 1e-6
|
||||
|
||||
|
||||
def test_small_triangles_on_float32_axis_cover_every_cell():
|
||||
"""실데이터처럼 격자보다 작은 삼각형이 촘촘할 때도 누락이 없어야 한다."""
|
||||
x_coords = np.linspace(183433.6, 183463.6, 31, dtype=np.float32)
|
||||
y_coords = np.linspace(489168.8, 489198.8, 31, dtype=np.float32)
|
||||
|
||||
# 0.5m 간격 정점망을 사각형→삼각형 2개로 쪼개 촘촘한 TIN을 흉내낸다.
|
||||
gx = np.arange(float(x_coords[0]) - 1.0, float(x_coords[-1]) + 1.5, 0.5)
|
||||
gy = np.arange(float(y_coords[0]) - 1.0, float(y_coords[-1]) + 1.5, 0.5)
|
||||
mx, my = np.meshgrid(gx, gy)
|
||||
verts = np.column_stack([mx.ravel(), my.ravel(), _plane(mx.ravel(), my.ravel())])
|
||||
n_col = len(gx)
|
||||
tris = []
|
||||
for r in range(len(gy) - 1):
|
||||
for c in range(n_col - 1):
|
||||
p00, p10 = r * n_col + c, r * n_col + c + 1
|
||||
p01, p11 = (r + 1) * n_col + c, (r + 1) * n_col + c + 1
|
||||
tris.append([p00, p10, p11])
|
||||
tris.append([p00, p11, p01])
|
||||
|
||||
z_grid = rasterize_triangle_mesh(verts, np.array(tris), x_coords, y_coords)
|
||||
|
||||
assert np.isfinite(z_grid).all(), f"빈 셀 {int((~np.isfinite(z_grid)).sum()):,}개"
|
||||
xx, yy = np.meshgrid(np.asarray(x_coords, np.float64), np.asarray(y_coords, np.float64))
|
||||
assert np.abs(z_grid - _plane(xx, yy)).max() < 1e-6
|
||||
|
||||
|
||||
def test_descending_axis_is_supported():
|
||||
"""내림차순 축(예: 북쪽이 위인 y축)도 같은 결과를 낸다."""
|
||||
corners = [(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)]
|
||||
verts = np.array([[x, y, _plane(x, y)] for x, y in corners])
|
||||
tris = np.array([[0, 1, 2], [0, 2, 3]])
|
||||
x_coords = np.linspace(0.0, 4.0, 5)
|
||||
y_desc = np.linspace(4.0, 0.0, 5)
|
||||
|
||||
z_grid = rasterize_triangle_mesh(verts, tris, x_coords, y_desc)
|
||||
|
||||
xx, yy = np.meshgrid(x_coords, y_desc)
|
||||
assert np.isfinite(z_grid).all()
|
||||
assert np.allclose(z_grid, _plane(xx, yy), atol=1e-9)
|
||||
|
||||
|
||||
def test_empty_mesh_returns_all_nan():
|
||||
x_coords, y_coords = _axes(0.0, 3.0, 0.0, 3.0)
|
||||
z_grid = rasterize_triangle_mesh(
|
||||
np.zeros((0, 3)), np.zeros((0, 3), dtype=np.int64), x_coords, y_coords
|
||||
)
|
||||
assert z_grid.shape == (len(y_coords), len(x_coords))
|
||||
assert np.isnan(z_grid).all()
|
||||
@@ -1,31 +1,18 @@
|
||||
"""B04 지면 필터 회귀 테스트 — 고정 상수 때문에 지면점이 사라지던 결함 방지.
|
||||
|
||||
원 결함(2026-09-01): CSF 하강 예산이 0.3185m x 150회 = 47.8m로 고정돼 기복이
|
||||
그보다 큰 산악지에서 천이 지면에 닿지 못했고(용화.las 지면점 0.06%),
|
||||
grid_min_z의 3x3 minimum_filter가 급경사에서 기준면을 경사만큼 파고들었다.
|
||||
"""
|
||||
"""B04 지면 필터 회귀 테스트 — 업체 분류 필터 · 자동 고름(CSF · 격자 최저는 608 에서 걷음)."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Filter_Classification import (
|
||||
filter_classification,
|
||||
has_classified_ground,
|
||||
)
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Filter_CSF import filter_csf
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Filter_Grid import filter_grid_min_z
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Ground import resolve_auto_source_filters
|
||||
|
||||
# CSF 고정 하강 예산 (옛 코드): 9.8 * 0.05 * 0.65 * 150
|
||||
LEGACY_CSF_DROP_BUDGET_M = 47.775
|
||||
|
||||
|
||||
def _sloped_ground(relief_m: float, slope: float = 0.3, spacing_m: float = 0.5) -> np.ndarray:
|
||||
"""지정한 경사로 relief_m 만큼 오르내리는 지면 점군을 만든다.
|
||||
|
||||
경사와 기복을 따로 잡는다 — 결함은 경사가 아니라 **절대 기복**이 CSF 하강
|
||||
예산을 넘을 때 터졌다. 점 간격은 격자·천 셀보다 촘촘하게 둔다(실제 LAS와 동일).
|
||||
셀 안 표고 편차가 임계값을 넘으면 필터와 무관하게 점이 떨어지기 때문이다.
|
||||
점 간격은 격자 칸보다 촘촘하게 둔다(실제 LAS와 동일).
|
||||
"""
|
||||
axis = np.arange(0.0, relief_m / slope, spacing_m, dtype=np.float64)
|
||||
x, y = np.meshgrid(axis, axis)
|
||||
@@ -49,51 +36,6 @@ def _structured(points: np.ndarray, classification: np.ndarray | None = None) ->
|
||||
return data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("relief_m", [20.0, 120.0])
|
||||
def test_csf_recovers_ground_regardless_of_relief(relief_m: float) -> None:
|
||||
"""기복이 옛 고정 예산(47.8m)을 넘어도 지면이 살아남는다."""
|
||||
ground = _sloped_ground(relief_m)
|
||||
mask = filter_csf(_structured(ground))
|
||||
|
||||
assert mask.mean() > 0.5, f"기복 {relief_m}m 에서 지면 회수율 {mask.mean():.1%}"
|
||||
# 옛 결함은 낮은 쪽 띠만 남기고 높은 쪽을 통째로 버렸다.
|
||||
high_side = ground[:, 2] > ground[:, 2].min() + LEGACY_CSF_DROP_BUDGET_M
|
||||
if high_side.any():
|
||||
assert mask[high_side].mean() > 0.5, "옛 하강 예산 위쪽 지면이 잡히지 않는다"
|
||||
|
||||
|
||||
def test_csf_rejects_canopy_points() -> None:
|
||||
"""6단계 수목 필터가 실제로 동작한다 (피연산자 역전 시 항상 참이 되던 자리)."""
|
||||
ground = _sloped_ground(60.0, slope=0.3)
|
||||
canopy = ground.copy()
|
||||
canopy[:, 2] += 12.0
|
||||
points = np.vstack([ground, canopy])
|
||||
mask = filter_csf(_structured(points))
|
||||
|
||||
is_canopy = np.zeros(len(points), dtype=bool)
|
||||
is_canopy[len(ground) :] = True
|
||||
assert not mask[is_canopy].any(), "수목 반사점이 지면으로 분류됐다"
|
||||
assert mask[~is_canopy].mean() > 0.5
|
||||
|
||||
|
||||
def test_grid_min_z_keeps_ground_on_steep_slope() -> None:
|
||||
"""급경사에서 3x3 최소필터가 기준면을 파고들던 결함 방지."""
|
||||
# 경사 100%. 셀 1m 안 표고 편차는 1.0m로 임계값(1.5m) 이내지만, 옛 코드는
|
||||
# 3x3 최소필터가 2m 밖 셀 값을 끌어와 기준면을 2.0m 낮췄다 -> 임계값 초과.
|
||||
ground = _sloped_ground(120.0, slope=1.0)
|
||||
mask = filter_grid_min_z(_structured(ground), cell_size=1.0, height_threshold=1.5)
|
||||
assert mask.mean() > 0.9, f"급경사 지면 회수율 {mask.mean():.1%}"
|
||||
|
||||
|
||||
def test_grid_min_z_rejects_points_above_threshold() -> None:
|
||||
ground = _sloped_ground(10.0)
|
||||
above = ground.copy()
|
||||
above[:, 2] += 5.0 # 임계값 1.5m 초과
|
||||
points = np.vstack([ground, above])
|
||||
mask = filter_grid_min_z(_structured(points))
|
||||
assert not mask[len(ground) :].any()
|
||||
|
||||
|
||||
def test_classification_filter_reads_class_two() -> None:
|
||||
points = _sloped_ground(10.0)
|
||||
classification = np.ones(len(points), dtype=np.uint8)
|
||||
|
||||
@@ -296,7 +296,7 @@ def _generate(project_root, options=None):
|
||||
return run_section_generation(
|
||||
project_root,
|
||||
"B05_Profile/route/route_main.geojson",
|
||||
"csf",
|
||||
"ptd",
|
||||
"dtm",
|
||||
True,
|
||||
options=options or SectionGenerationOptions(station_interval_m=20.0),
|
||||
|
||||
@@ -71,7 +71,7 @@ def test_request_wins_over_stored():
|
||||
def test_schema_defaults_to_none_and_ships_the_flag():
|
||||
"""스키마 기본은 None(미지정) — 저장값을 덮어쓰지 않는다."""
|
||||
request = RouteSolveRequest(
|
||||
filter_key="csf",
|
||||
filter_key="ptd",
|
||||
bp={"x": 0.0, "y": 0.0},
|
||||
ep={"x": 100.0, "y": 0.0},
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ def test_샘플링_조건이_없으면_None(tmp_path: Path) -> None:
|
||||
path.write_text(json.dumps({"filter_key": "", "method": "dtm"}), encoding="utf-8")
|
||||
assert load_sampling_snapshot(tmp_path) is None
|
||||
path.write_text(
|
||||
json.dumps({"filter_key": "csf", "method": "tin", "smooth": True}), encoding="utf-8"
|
||||
json.dumps({"filter_key": "ptd", "method": "implicit", "smooth": True}), encoding="utf-8"
|
||||
)
|
||||
snapshot = load_sampling_snapshot(tmp_path)
|
||||
assert snapshot is not None and snapshot["method"] == "tin" and snapshot["smooth"] is True
|
||||
assert snapshot is not None and snapshot["method"] == "implicit" and snapshot["smooth"] is True
|
||||
|
||||
@@ -104,7 +104,10 @@ def test_every_mode_keeps_grade_limit_on_hill(hill: Terrain, mode: str) -> None:
|
||||
_kept(result)
|
||||
metrics = result["metrics"]
|
||||
assert metrics["over_limit"]["total"] == 0
|
||||
assert metrics["max_grade_pct"] <= GRADE * 100.0 + 1e-6
|
||||
# 영선 = 상한 기울기를 걷는 선 — 곡선 넣기로 조금 넘을 수 있음(원지반 기울기 참고만 · 419 ·
|
||||
# 측점 맞춤 없는 가이드 선 · 769-1)
|
||||
if mode != "gradeline":
|
||||
assert metrics["max_grade_pct"] <= GRADE * 100.0 + 1e-6
|
||||
# 기점 · 종점은 옮기지 않고 복도(100 m) 밖으로 안 나감
|
||||
assert result["planned"][0] == [50.0, 200.0]
|
||||
assert result["planned"][-1] == [750.0, 200.0]
|
||||
|
||||
@@ -72,14 +72,14 @@ def test_sheet_surface_npz_and_sampler_roundtrip(sheet_project: dict) -> None:
|
||||
sheet_project["models"],
|
||||
route_xy,
|
||||
EPSG,
|
||||
methods=["tin_sheet"],
|
||||
methods=["laplace"],
|
||||
)
|
||||
assert len(models) == 1
|
||||
model = models[0]
|
||||
assert model["model_type"] == "dtm"
|
||||
assert model["generation_params"]["source_filter"] == "sheet_tin_sheet"
|
||||
assert model["generation_params"]["source_filter"] == "sheet_laplace"
|
||||
|
||||
npz_path = sheet_project["models"] / "dtm_sheet_tin_sheet.npz"
|
||||
npz_path = sheet_project["models"] / "dtm_sheet_laplace.npz"
|
||||
assert npz_path.is_file()
|
||||
with np.load(npz_path) as data:
|
||||
for key in ("x", "y", "z", "valid_mask"):
|
||||
@@ -90,17 +90,17 @@ def test_sheet_surface_npz_and_sampler_roundtrip(sheet_project: dict) -> None:
|
||||
assert data["z"].shape == (len(y), len(x))
|
||||
|
||||
# 프리뷰 glb
|
||||
assert (sheet_project["models"] / "dtm_sheet_tin_sheet_preview.glb").stat().st_size > 0
|
||||
assert (sheet_project["models"] / "dtm_sheet_laplace_preview.glb").stat().st_size > 0
|
||||
|
||||
# 종·횡단 공용 sampler로 왕복 — 등고선 안쪽은 유효 표고.
|
||||
# 최고 등고선(120) 안쪽은 마루 캡(경사 연장)으로 최대 +간격-0.5m까지 올라갈 수 있다.
|
||||
sampler = build_surface_sampler(sheet_project["models"], "sheet_tin_sheet", "dtm", smooth=False)
|
||||
sampler = build_surface_sampler(sheet_project["models"], "sheet_laplace", "dtm", smooth=False)
|
||||
inner = np.array([[X0, Y0 + 200.0], [X0 + 100.0, Y0 - 100.0]], dtype=np.float64)
|
||||
z, valid = sampler.sample_xy(inner)
|
||||
assert valid.all()
|
||||
assert np.all((z >= 100.0 - 1e-6) & (z <= 124.5 + 1e-3))
|
||||
|
||||
# TIN 바깥(등고선 밖 + margin 밖)은 무효
|
||||
# 바깥(등고선 밖 + margin 밖)은 무효
|
||||
outer = np.array([[X0 + 10000.0, Y0]], dtype=np.float64)
|
||||
_, outer_valid = sampler.sample_xy(outer)
|
||||
assert not outer_valid.any()
|
||||
@@ -185,32 +185,3 @@ def test_cone_reproduction_beats_over_relaxation() -> None:
|
||||
relax_laplace(heavy, np.isfinite(burned), 200)
|
||||
# 수렴시키면 harmonic 해로 끌려가 오차가 눈에 띄게 커진다
|
||||
assert float(np.abs(heavy - truth)[band].mean()) > base_error
|
||||
|
||||
|
||||
def test_tin_sheet_follows_source_contour_lines(sheet_project: dict) -> None:
|
||||
"""TIN(도엽선)은 원본 등고선 자리에서 그 표고를 그대로 낸다 — 격자 계단을 타지 않는다.
|
||||
|
||||
링을 셀 격자와 어긋난 위치(+0.37m)에 두어, 격자에 구운 라인 셀이 아니라 벡터
|
||||
정점을 쓰는지 가른다.
|
||||
"""
|
||||
route_xy = np.array([[X0 - 200.0, Y0 - 200.0], [X0 + 200.0, Y0 + 200.0]], dtype=np.float64)
|
||||
models = build_sheet_surface_model(
|
||||
sheet_project["root"],
|
||||
sheet_project["processed"],
|
||||
sheet_project["models"],
|
||||
route_xy,
|
||||
EPSG,
|
||||
methods=["tin_sheet"],
|
||||
)
|
||||
assert len(models) == 1
|
||||
|
||||
sampler = build_surface_sampler(sheet_project["models"], "sheet_tin_sheet", "dtm", smooth=False)
|
||||
# 각 링 변 위의 점 — 표고는 그 링의 값이어야 한다
|
||||
probes, expected = [], []
|
||||
for half, elev in ((300.0, 110.0), (150.0, 120.0)):
|
||||
for offset in (-100.0, 0.0, 100.0):
|
||||
probes.append([X0 + offset + 0.37, Y0 - half + 0.37])
|
||||
expected.append(elev)
|
||||
z, valid = sampler.sample_xy(np.asarray(probes))
|
||||
assert valid.all()
|
||||
assert np.abs(z - np.asarray(expected)).max() < 1.0
|
||||
|
||||
Reference in New Issue
Block a user