- 필터 = 업체 분류 · 점진 TIN · 둘의 다듬기 판만 · 분석 허용 목록 = Ground 등록부(available_filters) 한 벌 - 표현 = DTM · implicit 만 · 평활 = DTM 만 · config 기본 목록 · PRECOMPUTE · 설정 사전 맞춤(설정 지문 바뀜 — 768 에서 다시 지음) - 등고선 = 격자 · 굳힌 격자만(삼각망 굽기 · 외곽 맞춤 · implicit RBF 폴백 걷음) · 표본기 · 경로 찾기 tin · nurbs · meshfree 갈래 걷음 - 수치지형도 = 라플라스만(TIN 둘 · TPS · ANUDEM형 · 다중해상도 걷음) · B04 단추 하나 - 안 부르는 길 — B04 JSON 점군 GET · ground-stats GET + TS · B07 옛 계획평면도(라이다) 짓기 · lidar_source · LIDAR_ID · plan_lidar 종류 이름 · SOURCE_FILTER_LABELS · MODEL_METHODS(py) · MESH_* 설정 - 옛 manifest 의 지운 갈래 줄은 DB 로 되살리지 않음 · 옛 csf 행은 미리보기 · 등고선 그대로 열림 · 지운 이름 요청 = 400 - 낡은 글 — Ground · Pipeline 머리 · WF1 「없으면 csf」 · B07 음영 기본 「csf」 → 빈 값 · 등고선 폴백 「csf」 → 404 · B04 3D 보기 처음값 · B05 Schema · B06 Prebuild · 씨닝 설명 - 시험 지움 2 — test_b04_contour_raster · test_b04_contour_equivalence(지운 삼각망 굽기 · 없는 프로젝트를 봐 건너뛰던 것) - 시험 덜어냄 — test_b04_ground_filters CSF · 격자 최저 네 경우(지운 필터) - 시험 고침 — 허용 목록 · 형 검사에 걸린 csf · tin · grid_min_z · meshfree · tin_sheet 견본을 ptd · implicit · laplace 로(54_22 · 238_1 · 443 · 342_2 · 346_3 · 351_1 · 442_2 · 271 + helper · 344_1 · 133_1 · 343_2 · 637 · 754_1 · 763_1 깨진 tin → 깨진 implicit · 763_3 · b05 둘 · b06 · sheet_surface TIN(도엽선) 경우 지움) · 이름만 바꿈(273_2 · 343_1 · 343_3 · 624_1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
247 lines
9.3 KiB
Python
247 lines
9.3 KiB
Python
"""343-1 같은 파일 다시 올림 = 전처리 결과 살림 · 점군 안 풂 · 도엽 병합 · 타일 기억 열쇠.
|
|
|
|
서버 함수만 시험 — 실제 재올림은 하지 않음(완료 메일).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
from B03_FileInput.B03_FileInput_Router_Helpers import (
|
|
OutputsWouldBeDiscarded,
|
|
_confirm_replace_response,
|
|
describe_existing_outputs,
|
|
preprocess_input_unchanged,
|
|
)
|
|
from B04_PreProcess.B04_PreProcess_Engine import (
|
|
_key_matches,
|
|
_LazyStructured,
|
|
_LazyXyz,
|
|
_source_identity,
|
|
_structured_stats,
|
|
)
|
|
from B04_PreProcess.B04_PreProcess_Engine_Ground import summarize_masks
|
|
from common_util.common_util_project_reset import purge_output_folders, purge_output_records
|
|
|
|
|
|
def _project(tmp_path: Path) -> tuple[Path, Path]:
|
|
las = tmp_path / "B03_FileInput" / "input" / "a.las"
|
|
las.parent.mkdir(parents=True)
|
|
las.write_bytes(b"LASF" + b"0" * 100)
|
|
processed = tmp_path / "B04_PreProcess" / "processed"
|
|
processed.mkdir(parents=True)
|
|
(processed / "source_identity.json").write_text(
|
|
json.dumps(_source_identity([las])), encoding="utf-8"
|
|
)
|
|
return tmp_path, las
|
|
|
|
|
|
def test_같은_지형_파일이면_전처리_살림(tmp_path: Path) -> None:
|
|
root, las = _project(tmp_path)
|
|
assert preprocess_input_unchanged(root, [las]) is True
|
|
|
|
|
|
def test_지형_파일이_바뀌면_살리지_않음(tmp_path: Path) -> None:
|
|
root, las = _project(tmp_path)
|
|
stat = las.stat()
|
|
os.utime(las, ns=(stat.st_atime_ns, stat.st_mtime_ns + 5_000_000_000))
|
|
assert preprocess_input_unchanged(root, [las]) is False
|
|
assert preprocess_input_unchanged(root, []) is False
|
|
|
|
|
|
def test_전처리_기록이_없으면_살리지_않음(tmp_path: Path) -> None:
|
|
root, las = _project(tmp_path)
|
|
(root / "B04_PreProcess" / "processed" / "source_identity.json").unlink()
|
|
assert preprocess_input_unchanged(root, [las]) is False
|
|
|
|
|
|
def test_살리면_확인_창_목록에서_전처리_빠짐(tmp_path: Path) -> None:
|
|
root, _ = _project(tmp_path)
|
|
(root / "B05_Profile").mkdir()
|
|
(root / "B05_Profile" / "route.json").write_text("{}", encoding="utf-8")
|
|
assert "지표면·전처리 결과" in describe_existing_outputs(root)
|
|
kept = describe_existing_outputs(root, keep_preprocess=True)
|
|
assert "지표면·전처리 결과" not in kept
|
|
assert "노선·종단 설계" in kept
|
|
|
|
|
|
def test_확인_응답에_전처리_살림_표시() -> None:
|
|
body = json.loads(_confirm_replace_response(OutputsWouldBeDiscarded(["횡단 설계"], True)).body)
|
|
assert body["preprocess_kept"] is True
|
|
body = json.loads(_confirm_replace_response(OutputsWouldBeDiscarded(["횡단 설계"])).body)
|
|
assert body["preprocess_kept"] is False
|
|
|
|
|
|
def test_폴더_지우기는_전처리만_남김(tmp_path: Path) -> None:
|
|
root, _ = _project(tmp_path)
|
|
(root / "B05_Profile").mkdir()
|
|
(root / "B05_Profile" / "route.json").write_text("{}", encoding="utf-8")
|
|
removed = purge_output_folders(root, keep_preprocess=True)
|
|
assert "B04_PreProcess" not in removed
|
|
assert (root / "B04_PreProcess" / "processed" / "source_identity.json").is_file()
|
|
assert not (root / "B05_Profile" / "route.json").exists()
|
|
assert "B04_PreProcess" in purge_output_folders(root)
|
|
assert not (root / "B04_PreProcess" / "processed").exists()
|
|
|
|
|
|
class _Cursor:
|
|
def __init__(self, log: list[str]) -> None:
|
|
self.log = log
|
|
self.rowcount = 0
|
|
|
|
async def __aenter__(self) -> "_Cursor":
|
|
return self
|
|
|
|
async def __aexit__(self, *_: object) -> None:
|
|
return None
|
|
|
|
async def execute(self, sql: str, _params: object = None) -> None:
|
|
self.log.append(sql)
|
|
|
|
|
|
class _Connection:
|
|
def __init__(self) -> None:
|
|
self.log: list[str] = []
|
|
|
|
def cursor(self) -> _Cursor:
|
|
return _Cursor(self.log)
|
|
|
|
|
|
def test_DB_지우기는_지표면_행을_남김() -> None:
|
|
connection = _Connection()
|
|
asyncio.run(purge_output_records(connection, "p", keep_preprocess=True)) # type: ignore[arg-type]
|
|
joined = "\n".join(connection.log)
|
|
assert "surface_models" not in joined
|
|
assert "processed_point_cloud" not in joined
|
|
assert "DELETE FROM routes" in joined
|
|
connection = _Connection()
|
|
asyncio.run(purge_output_records(connection, "p")) # type: ignore[arg-type]
|
|
assert "surface_models" in "\n".join(connection.log)
|
|
|
|
|
|
def test_점_수_평균Z_저장값_두번째는_안_풂(tmp_path: Path) -> None:
|
|
path = tmp_path / "structured.npz"
|
|
xyz = np.array([[0, 0, 1], [1, 1, 3]], dtype=np.float32)
|
|
np.savez_compressed(path, xyz=xyz, bounds=np.zeros((3, 2)))
|
|
with np.load(path) as structured:
|
|
stats, loaded = _structured_stats(structured, path)
|
|
assert stats["point_count"] == 2 and stats["mean_z"] == 2.0
|
|
assert loaded is not None
|
|
with np.load(path) as structured:
|
|
stats, loaded = _structured_stats(structured, path)
|
|
assert loaded is None and stats["point_count"] == 2
|
|
# 구조화가 다시 돌면(파일이 바뀌면) 다시 셈
|
|
np.savez_compressed(path, xyz=np.zeros((5, 3), np.float32), bounds=np.zeros((3, 2)))
|
|
with np.load(path) as structured:
|
|
stats, loaded = _structured_stats(structured, path)
|
|
assert stats["point_count"] == 5 and loaded is not None
|
|
|
|
|
|
class _NoXyz(dict):
|
|
files = ["xyz", "bounds", "classification"]
|
|
|
|
def __getitem__(self, key: str) -> object:
|
|
if key == "xyz":
|
|
raise AssertionError("점군을 풀면 안 됨")
|
|
return dict.__getitem__(self, key)
|
|
|
|
|
|
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, {"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
|
|
|
|
|
|
def test_점군은_쓸_때_풂() -> None:
|
|
xyz = np.arange(9, dtype=np.float32).reshape(3, 3)
|
|
lazy = _LazyXyz({"xyz": xyz}, 3)
|
|
assert lazy.shape == (3, 3) and lazy.ndim == 2
|
|
assert np.asarray(lazy, dtype=np.float64)[2, 2] == 8.0
|
|
assert lazy[1, 0] == 3.0
|
|
|
|
|
|
def test_결과_옆_열쇠_대조(tmp_path: Path) -> None:
|
|
key_path = tmp_path / "sheet_merge.key.json"
|
|
key = {"sheets": ["37612001", "37612002"]}
|
|
assert _key_matches(key_path, key) is False
|
|
key_path.write_text(json.dumps(key), encoding="utf-8")
|
|
assert _key_matches(key_path, key) is True
|
|
assert _key_matches(key_path, {"sheets": ["37612001"]}) is False
|
|
|
|
|
|
def test_지도_타일_기억은_파일이_바뀌면_다시_읽음(tmp_path: Path) -> None:
|
|
from B04_PreProcess.B04_PreProcess_Engine_MvtHelper import load_geojson_cached
|
|
|
|
path = tmp_path / "등고선_bounds.geojson"
|
|
|
|
def write(count: int) -> None:
|
|
features = [
|
|
{
|
|
"type": "Feature",
|
|
"properties": {"h": index},
|
|
"geometry": {"type": "Point", "coordinates": [127.0 + index * 0.01, 37.0]},
|
|
}
|
|
for index in range(count)
|
|
]
|
|
path.write_text(
|
|
json.dumps({"type": "FeatureCollection", "features": features}), encoding="utf-8"
|
|
)
|
|
|
|
write(1)
|
|
assert len(load_geojson_cached(path, "t343_contour")) == 1
|
|
assert len(load_geojson_cached(path, "t343_contour")) == 1
|
|
write(3)
|
|
stat = path.stat()
|
|
os.utime(path, ns=(stat.st_atime_ns, stat.st_mtime_ns + 5_000_000_000))
|
|
assert len(load_geojson_cached(path, "t343_contour")) == 3
|
|
|
|
|
|
def test_다시_돌면_같은_결과_점군_안_풂(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
"""작은 LAS 로 전처리 두 번 — 두 번째는 저장본만 읽고 점군 xyz 를 풀지 않음."""
|
|
import laspy
|
|
|
|
import B04_PreProcess.B04_PreProcess_Engine as engine
|
|
import B04_PreProcess.B04_PreProcess_Engine_SheetSurface as sheet
|
|
|
|
rng = np.random.default_rng(1)
|
|
count = 4000
|
|
x = rng.uniform(200000, 200060, count)
|
|
y = rng.uniform(500000, 500060, count)
|
|
header = laspy.LasHeader(point_format=3, version="1.2")
|
|
header.offsets = [200000, 500000, 0]
|
|
header.scales = [0.01, 0.01, 0.01]
|
|
las_data = laspy.LasData(header)
|
|
las_data.x, las_data.y, las_data.z = x, y, 100 + 0.1 * x - 0.1 * 200000
|
|
las_data.classification = np.full(count, 2, np.uint8)
|
|
las_path = tmp_path / "B03_FileInput" / "input" / "t.las"
|
|
las_path.parent.mkdir(parents=True)
|
|
las_data.write(las_path)
|
|
|
|
monkeypatch.setattr(engine, "download_geodata", lambda *a, **k: None)
|
|
monkeypatch.setattr(sheet, "build_sheet_surface_from_route", lambda *a, **k: [])
|
|
first = engine.run_surface_analysis(tmp_path, las_path, source_filters=None, methods=["dtm"])
|
|
|
|
real_lazy_load = engine._LazyXyz._load
|
|
calls: list[int] = []
|
|
|
|
def counting_load(self): # type: ignore[no-untyped-def]
|
|
calls.append(1)
|
|
return real_lazy_load(self)
|
|
|
|
monkeypatch.setattr(engine._LazyXyz, "_load", counting_load)
|
|
second = engine.run_surface_analysis(tmp_path, las_path, source_filters=None, methods=["dtm"])
|
|
assert first["models"] and calls == []
|
|
assert second["processed"] == first["processed"]
|
|
assert second["ground_summary"] == first["ground_summary"]
|
|
assert [m["model_file_path"] for m in second["models"]] == [
|
|
m["model_file_path"] for m in first["models"]
|
|
]
|