"""절단 여유 30m → 3m 로 낮췄을 때 끝단 지반고가 튀지 않는지 (2026-09-04 사용자 지시). 용화_LAS 프로젝트의 확정 지표면으로 실측한다. 판정은 「3m 로 남긴 양 끝 20m 구간의 지반고 변화율이 노선 안쪽 구간과 같은 수준인가」 — 가장자리 점 밀도가 떨어져 값이 못 미더우면 여기서 눈에 띄게 튄다. """ import asyncio import math import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.append(str(ROOT)) PROJECT_ID = "5cff3920-a181-4a3d-bec0-e0ac4082b75d" # 용화_LAS def _project_root_and_surface(): import aiomysql from config.config_db import DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USER from common_util.common_util_storage import resolve_stored_project_path async def run(): conn = await aiomysql.connect( host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD, db=DB_NAME, charset="utf8mb4", ) try: async with conn.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( "SELECT storage_path FROM projects WHERE id = %s", (PROJECT_ID,) ) row = await cursor.fetchone() from common_util.common_util_surface_confirmation import get_surface_confirmation_params params = await get_surface_confirmation_params(conn, PROJECT_ID) return row, params finally: conn.close() row, params = asyncio.run(run()) if not row: pytest.skip("용화_LAS 프로젝트가 없습니다.") return Path(resolve_stored_project_path(row["storage_path"])), params def _sample_z(sampler, points): import numpy as np z, valid = sampler.sample_xy(np.asarray(points, dtype=np.float64)) return np.asarray(z, dtype=float), np.asarray(valid, dtype=bool) def _slopes(sampler, points): """이웃 정점 사이 지반고 변화율(m/m) 목록.""" z, valid = _sample_z(sampler, points) out = [] for i in range(1, len(points)): if not (valid[i] and valid[i - 1]): continue d = math.dist(points[i - 1], points[i]) if d > 0.5: out.append(abs(z[i] - z[i - 1]) / d) return out def _head_tail_within(points, metres): """양 끝에서 `metres` 안에 드는 정점만.""" acc = [0.0] for i in range(1, len(points)): acc.append(acc[-1] + math.dist(points[i - 1], points[i])) total = acc[-1] head = [p for p, a in zip(points, acc) if a <= metres] tail = [p for p, a in zip(points, acc) if total - a <= metres] return head, tail, total def test_edge_trim_3m_ends_are_not_spiky(): from common_util.common_util_route_geometry import ( find_planned_route_file, read_planned_route, trim_route_to_surface, ) from common_util.common_util_surface_sampler import build_surface_sampler from B04_PreProcess.B04_PreProcess_Engine_Extent import project_epsg_from_prj project_root, params = _project_root_and_surface() route_file = find_planned_route_file(project_root / "B03_FileInput" / "input") if route_file is None: pytest.skip("계획노선 파일이 없습니다.") planned = read_planned_route(route_file) points = [(float(v.x), float(v.y)) for v in planned.vertices] target_crs = project_epsg_from_prj(project_root) source_crs = planned.crs_input or target_crs if source_crs.upper() != target_crs.upper(): from pyproj import Transformer transformer = Transformer.from_crs(source_crs, target_crs, always_xy=True) points = [transformer.transform(x, y) for x, y in points] sampler = build_surface_sampler( project_root / "B04_PreProcess" / "models", str(params["source_filter"]), str(params["method"]), bool(params["smooth"]), ) trimmed_30 = trim_route_to_surface(points, sampler, 30.0) trimmed_3 = trim_route_to_surface(points, sampler, 3.0) assert len(trimmed_3) >= 2 head, tail, total_3 = _head_tail_within(trimmed_3, 20.0) _, _, total_30 = _head_tail_within(trimmed_30, 20.0) print(f"\n연장: 30m 트림 {total_30:.1f}m / 3m 트림 {total_3:.1f}m (차이 {total_3 - total_30:.1f}m)") inner = _slopes(sampler, trimmed_3[len(trimmed_3) // 4 : 3 * len(trimmed_3) // 4]) ends = _slopes(sampler, head) + _slopes(sampler, tail) assert inner and ends inner_max = max(inner) ends_max = max(ends) print(f"지반고 변화율 최대: 안쪽 {inner_max:.3f} / 양 끝 20m {ends_max:.3f}") # 끝단이 안쪽보다 크게 튀지 않아야 한다(가장자리 밀도 저하 확인). assert ends_max <= max(inner_max * 1.5, inner_max + 0.05) def test_config_default_is_3m(): from config.config_system_terrain import SURFACE_ROUTE_EDGE_TRIM_M assert SURFACE_ROUTE_EDGE_TRIM_M == 3.0