"""758-1 예정노선 따라가기 곡선 최적(요구 598-1) — 「토공 최소」와 같은 큰 R · 합치기. ./venv/Scripts/python.exe -m pytest resources/tester/test_758_1_follow_merge.py -q - 합성 언덕 지형 · 굽은 예정노선 → 곡선 수 줆 · MC 모두 일반측점 · 최소 R 지킴. - 한 함수 — 체인 처음(`followed_table` + 지형) = `/route/follow`(`follow_route_line`) = 추천 「예상노선 추적」(`generate_initial_route` follow) 노드 같음. - 지형 못 읽으면(None) 옛 길(순수 기하 `snapped_initial`) 그대로. - 글 대조 — 체인 · `/route/plan` · `/route/replan` 은 `initial_land` · `/route/follow` 는 `follow_land` 를 넘김. """ from __future__ import annotations import math from pathlib import Path import numpy as np from shapely.geometry import LineString from B05_Profile.B05_Profile_Engine_RouteInitial import generate_initial_route, resolve_inputs from B05_Profile.B05_Profile_Engine_RouteInitial_Follow import FollowLand, merged_follow from B05_Profile.B05_Profile_Engine_RouteInitial_Search import Terrain from B05_Profile.B05_Profile_Engine_RouteInitial_Station import ( off_station_count, snapped_initial, ) from B05_Profile.B05_Profile_Route_Polyline_Write import followed_table from B05_Profile.B05_Profile_Router_RouteFollow import follow_route_line ROOT = Path(__file__).resolve().parents[2] RADIUS = 12.0 # 3 m 간격(조밀화 3.6 m 안 — `/route/follow` 가 더 안 쪼갬 · 체인 예정노선과 같은 입력) EXPECTED = [(float(x), 200.0 + 40.0 * math.sin(x / 60.0)) for x in np.arange(50.0, 752.0, 3.0)] def _land() -> FollowLand: x = np.arange(0.0, 802.0, 2.0) y = np.arange(0.0, 402.0, 2.0) xx, yy = np.meshgrid(x, y) z = 40.0 * np.exp(-((xx - 400.0) ** 2 + (yy - 200.0) ** 2) / 90.0**2) terrain = Terrain(x, y, z, np.ones(xx.shape, dtype=bool)) return FollowLand(terrain, [], 0.14, *resolve_inputs(0.14, None, None, None)) def _nodes(line) -> list[tuple[float, float]]: return [(round(node.x, 3), round(node.y, 3)) for node in line.nodes] def test_곡선_줆_측점_위_최소_R(): plain = followed_table(EXPECTED, RADIUS)[0] merged = merged_follow(EXPECTED, RADIUS, _land()) assert len(merged.curves) < len(plain.curves) assert off_station_count(merged) == 0 assert min(curve.radius_m for curve in merged.curves) >= RADIUS - 1e-6 gap = LineString(EXPECTED).hausdorff_distance(LineString(merged.vertices)) assert gap < 30.0 # 예정노선 따름(토공 최소 후보처럼 멀리 안 감) def test_체인_follow_추천_한_함수(): land = _land() chain = followed_table(EXPECTED, RADIUS, land)[0] follow = follow_route_line(EXPECTED, RADIUS, land) shown = generate_initial_route( EXPECTED, land.terrain, [], mode="follow", weights=None, max_grade=0.14, min_radius_m=RADIUS, with_profile=False, ) want = _nodes(chain) assert [(round(n["x"], 3), round(n["y"], 3)) for n in follow["nodes"]] == want assert [(round(n["x"], 3), round(n["y"], 3)) for n in shown["nodes"]] == want assert follow["curve_count"] == len(chain.curves) == len(shown["curves"]) def test_지형_없으면_옛_길(): fit = snapped_initial(EXPECTED, RADIUS) line = followed_table(EXPECTED, RADIUS)[0] assert fit is not None assert [(round(x, 3), round(y, 3)) for x, y in fit[0]] == _nodes(line) def test_부르는_자리_지형_넘김(): def src(name: str) -> str: return (ROOT / name).read_text(encoding="utf-8") follow = src("B05_Profile/B05_Profile_Router_RouteFollow.py") assert "land = await follow_land(project_id, paths[0])" in follow assert "follow_route_line, points, radius_m, land" in follow replan = src("B05_Profile/B05_Profile_Router_Replan.py") assert replan.count("land = await initial_land(project_id, project_root)") == 2 assert "followed_table(points, radius_m, land)" in replan chain = src("B03_FileInput/B03_FileInput_Service_Chain.py") assert "_ensure_planned_initial, project_root, radius_m, land" in chain engine = src("B05_Profile/B05_Profile_Engine_RouteInitial.py") assert "merged_follow(expected, min_radius_m, land, grid)" in engine