- 새 `B05_Profile_Engine_RouteInitial_Follow.merged_follow` — 예정노선 폴리라인 → 큰 R(widen) → 꺾임만 곡선 → 같은 쪽 합치기(merge_same_side · 노선 횡단 토공 +10 % 멈춤) → 곡선 MC 일반측점 맞춤 · 잼 = 토공 최소와 같은 Guard(지형 · 계류 격자) · 같은 입력 기억 - 한 함수 — 체인 처음 · /route/plan · /route/replan(`initial_land` · 초기선 낡을 때만 지형 읽음) · /route/follow(`follow_land`) · 추천 「예상노선 추적」(follow_line · 묶음 격자 재사용) - 지형 못 읽으면 옛 길(순수 기하) · 기존 확정 노선 · 초기선 · 저장본은 안 바꿈(다음 체인 · 끌 때부터) - 화면 — 따라가기 뒤 지금 추천 = 예상노선 추적(「토공 최소」 꼬리표 남던 흠) · 걸음 사진에 실려 되돌리기 짝 - 엔진 합치기 잣대 셈(cross_total · cross_delta)은 새 파일 한 벌로 옮김(엔진 줄 줄어듦) - 시험: test_758_1 새 지킴 · test_383_1 은 체인 초기선도 같은 지형을 넘기게 고침(추천 추적 = 곡선 최적이라 순수 기하와 달라짐) · test_626_3 은 onFollow 새 꼴로 글 대조 고침 - 검증_sub1: 곡선 25 → 16 · 측점 16/16 · 예정노선과 최대 10.6 m · 끌 때 1.4~1.9 s Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
4.2 KiB
Python
101 lines
4.2 KiB
Python
"""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
|