Files
Aislo/resources/tester/test_624_4_plan_lidar.py
T
eomsangdonandClaude Opus 5.5 de8f2fee4a feat(D03): 예정노선 라이다 — LAS 올리기 · B04 엔진 지형 · terrain=lidar (PLAN 624-4 · 요구 530 · 531)
- POST /plan/lidar(LAS · LAZ + .prj · 예정노선만 · 다시 올리면 갈음) → 뒤에서 지형(진행 = /plan/progress lidar) · GET /plan/lidar
- 자리 = D 의 B04_PreProcess/lidar/(안은 B 와 같은 꼴) · LAS 좌표계 다르면 x · y 옮겨 적음 · DTM = surface_models 행(높이 격자)
- terrain=lidar 급경사 · 음영 · 0차곡 = 올린 DTM · 범위 밖 = 빈칸(수치지형도로 안 메움 · 결정 A⑥)
- B04 run_surface_analysis 에 maps 인자 덧(기본 True = B 그대로 · False = 지도 · GIS 받기 · 도엽 서피스 건너뜀)
- 시험 test_624_4_plan_lidar 4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:33:47 +09:00

129 lines
5.8 KiB
Python

"""PLAN 624-4 — D 예정노선 라이다: LAS 올리기 → B04 엔진 지형 → `terrain=lidar` 겹(요구 530 · 531).
D 몫 길 = `D03_PlanAnalysis_Lidar` · B04 엔진은 `maps=False` 인자만 덧(기본 = B 그대로).
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pytest
from fastapi import HTTPException
from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers
from D03_PlanAnalysis import D03_PlanAnalysis_Lidar as lidar
from D03_PlanAnalysis import D03_PlanAnalysis_TerrainSource as source
D_EPSG, LAS_EPSG = 5187, 5176 # D = GRS80 동부 · 업체 LAS = 베셀 동부(검증 용화와 같음)
def _write_las(path: Path, epsg: int | None, x0: float, y0: float) -> None:
"""두 덩이(가운데 빈 땅) 지면점 LAS — 비탈 0.3."""
import laspy
from pyproj import CRS
rng = np.random.default_rng(7)
xs, ys = [], []
for dx in (0.0, 160.0):
xs.append(rng.uniform(x0 + dx, x0 + dx + 60, 6000))
ys.append(rng.uniform(y0, y0 + 60, 6000))
x, y = np.concatenate(xs), np.concatenate(ys)
header = laspy.LasHeader(point_format=3, version="1.2")
header.offsets = [round(x0), round(y0), 0]
header.scales = [0.01, 0.01, 0.01]
if epsg is not None:
header.add_crs(CRS.from_epsg(epsg))
data = laspy.LasData(header)
data.x, data.y, data.z = x, y, 500 + 0.3 * (x - x0)
data.classification = np.full(len(x), 2, np.uint8)
path.parent.mkdir(parents=True, exist_ok=True)
data.write(path)
def _built(tmp_path: Path, monkeypatch, epsg: int | None = LAS_EPSG) -> dict: # type: ignore[no-untyped-def]
import B04_PreProcess.B04_PreProcess_Engine as engine
called: list[int] = []
monkeypatch.setattr(engine, "download_geodata", lambda *a, **k: called.append(1))
_write_las(lidar.lidar_root(tmp_path) / lidar.UPLOAD / "t.las", epsg, 208300.0, 367500.0)
got = lidar.build_lidar(tmp_path, D_EPSG, lambda *_: None)
assert called == [] # 지도 · GIS 받기 안 함
return got
def test_이름_고르기() -> None:
lidar.check_names(["a.las", "a.prj", "b.LAZ"])
for names in (["a.prj"], ["a.las", "a.txt"], ["a.las", "A.las"]):
with pytest.raises(HTTPException) as caught:
lidar.check_names(names)
assert caught.value.status_code == 400
def test_올리기_뒤_라이다_지형_B_꼴(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
got = _built(tmp_path, monkeypatch)
meta = got["meta"]
dem = lidar.lidar_dem(tmp_path)
assert dem is not None and dem.name == f"dtm_{meta['source_filter']}.npz"
assert dem.parent == lidar.lidar_root(tmp_path) / "B04_PreProcess" / "models"
assert meta["files"] == ["t.las"] and meta["point_count"] == 12000
assert not (lidar.lidar_root(tmp_path) / lidar.UPLOAD).exists()
# surface_models 행 경로 = 프로젝트 뿌리 기준
from B04_PreProcess.B04_PreProcess_Repository import _validate_stage_path
assert any(m["model_file_path"] == meta["model_file"] for m in got["models"])
assert all(_validate_stage_path(m["model_file_path"]) for m in got["models"])
# 좌표계 옮김 — 베셀 → GRS80 북 +10 만 m 언저리
from pyproj import Transformer
want = Transformer.from_crs(LAS_EPSG, D_EPSG, always_xy=True).transform(208300.0, 367500.0)
assert abs(meta["bounds"]["x_min"] - want[0]) < 1.0
assert abs(meta["bounds"]["y_min"] - want[1]) < 1.0
# 다시 올리면 갈음
_write_las(lidar.lidar_root(tmp_path) / lidar.UPLOAD / "u.las", None, 208300.0, 467500.0)
again = lidar.build_lidar(tmp_path, D_EPSG, lambda *_: None)["meta"]
assert again["files"] == ["u.las"] and abs(again["bounds"]["y_min"] - 467500.0) < 1.0
def test_terrain_lidar_급경사_0차곡_범위_밖_빈칸(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
with pytest.raises(HTTPException) as caught: # 안 올림 = 404
source.need_dem(source.plan_frame(tmp_path, D_EPSG, (0, 0, 1, 1), "lidar"))
assert caught.value.status_code == 404
_built(tmp_path, monkeypatch)
frame = source.plan_frame(tmp_path, D_EPSG, (0, 0, 1, 1), "lidar")
npz = source.need_dem(frame)
assert frame.source == "lidar" and npz == lidar.lidar_dem(tmp_path)
value = layers.slope_layer(tmp_path, npz.parent, D_EPSG, "slope", npz)
counts = value["counts"]
assert sum(counts) > 0 and max(range(len(counts)), key=counts.__getitem__) in (29, 30)
# 두 덩이 사이 = 빈칸(수치지형도로 안 메움)
with np.load(npz) as dem:
valid = np.asarray(dem["valid_mask"], bool)
x = np.asarray(dem["x"], float)
x0 = float(x[0])
gap = (x > x0 + 90) & (x < x0 + 130)
assert gap.any() and not valid[:, gap].any()
work, light, full = source.zero_order_calls(frame, 50.0, 5000.0)
assert work(*light) is None # 저장본 없음
value = work(*full)
assert value["cell_m"] > 0 and work(*light) == value # 다음 = 저장본
def test_B04_기본은_지도_받기_그대로(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
"""B 프로젝트 전처리(기본 인자) = 지도 · GIS 받기 · 도엽 서피스 그대로 부름."""
import B04_PreProcess.B04_PreProcess_Engine as engine
import B04_PreProcess.B04_PreProcess_Engine_SheetSurface as sheet
calls: list[str] = []
monkeypatch.setattr(engine, "download_geodata", lambda *a, **k: calls.append("maps"))
monkeypatch.setattr(
sheet, "build_sheet_surface_from_route", lambda *a, **k: calls.append("sheet") or []
)
las = tmp_path / "B03_FileInput" / "input" / "las" / "t.las"
_write_las(las, None, 200000.0, 500000.0)
engine.run_surface_analysis(tmp_path, las, source_filters=None, methods=["dtm"])
assert calls == ["maps", "sheet"]
calls.clear()
engine.run_surface_analysis(tmp_path, las, source_filters=None, methods=["dtm"], maps=False)
assert calls == []