Files
Aislo/resources/tester/test_383_1_route_station_default.py
T
eomsangdonandClaude Opus 5.5 c187f30e8e feat(758-1): 예정노선 따라가기에도 토공 최소 곡선 최적 — 큰 R · 같은 쪽 곡선 합치기 · 직접 잼 측점 맞춤
- 새 `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>
2026-11-03 08:38:17 +09:00

114 lines
5.0 KiB
Python

"""383-1 R = 일반측점 기본 제약 — 새 초기 노선 맞춤 + [측점 스냅] 켬 · 추천 후보 맞춤 · [적용] 켬.
./venv/Scripts/python.exe -m pytest resources/tester/test_383_1_route_station_default.py -q
새 사본(예상노선만) → 초기 노선 곡선 MC 가 일반측점 위(≈ 0 밖) · 곡선표 `station_snap` 켬 ·
추천 「예상노선 추적」(켬) 노드 = 초기 노선 노드 · 측점 밖 곡선 = 「못 지킨 구간」(station) ·
옛 사본(초기 노선 이미 있음)은 파일 · 깃발 그대로(말없이 측점 안 바뀜) · 추천 [적용] 은 스냅 켬.
합성 예상노선 · 지형으로 초기 노선과 추천 결과가 같은 기본 제약을 지키는지 확인한다.
"""
import json
import math
from pathlib import Path
from shapely.geometry import LineString, Point
from B05_Profile.B05_Profile_Engine_RouteInitial import generate_initial_route
from B05_Profile.B05_Profile_Engine_RouteInitial_Station import MC_TOL_M, STATION_M
from B05_Profile.B05_Profile_Engine_Sections import load_station_snap
from B05_Profile.B05_Profile_Router_Replan import (
_curves_path,
_ensure_planned_initial,
_nodes_path,
_write_planned_polyline,
)
from common_util.common_util_route_geometry import (
expected_route_csv_path,
planned_route_initial_path,
read_planned_route_csv,
write_route_csv,
)
ROOT = Path(__file__).resolve().parents[2]
RADIUS = 12.0
EXPECTED = [(float(x), 200.0 + 40.0 * math.sin(x / 60.0)) for x in range(50, 753, 20)]
def _copy_expected(tmp_path: Path) -> Path:
target = expected_route_csv_path(tmp_path)
write_route_csv(target, [{"x": x, "y": y} for x, y in EXPECTED])
return tmp_path
def _off_station(path: Path) -> tuple[int, int]:
"""(측점 밖 곡선 수, 곡선 수) — 저장 파일에서 다시 잼(MC = BC · EC 누가거리 가운데)."""
line = LineString([(v.x, v.y) for v in read_planned_route_csv(path).vertices])
curves = json.loads(_curves_path(path).read_text(encoding="utf-8"))["curves"]
taken, off = set(), 0
for curve in curves:
mc = (line.project(Point(curve["start"])) + line.project(Point(curve["end"]))) / 2
n = round(mc / STATION_M)
if n >= 1 and abs(mc - n * STATION_M) <= MC_TOL_M and n not in taken:
taken.add(n)
else:
off += 1
return off, len(curves)
def test_new_copy_initial_on_stations_and_flag_on(tmp_path: Path) -> None:
root = _copy_expected(tmp_path)
assert _ensure_planned_initial(root, RADIUS) is not None
initial = planned_route_initial_path(root)
off, total = _off_station(initial)
print(f"새 초기 노선 곡선 {total} · 측점 밖 {off}")
assert total and off <= 1
assert load_station_snap(root) is True
def test_follow_nodes_equal_initial(tmp_path: Path) -> None:
"""추천 「예상노선 추적」(측점 맞춤 켬) = 새 초기 노선(같은 지형 곡선 최적 · 758-1) · 측점 밖
곡선 수 = station 목록 수."""
import test_351_2_curve_stations as t351
from B05_Profile.B05_Profile_Engine_RouteInitial import resolve_inputs
from B05_Profile.B05_Profile_Engine_RouteInitial_Follow import FollowLand
root = _copy_expected(tmp_path)
terrain, streams = t351._terrain(), []
land = FollowLand(terrain, streams, 0.14, *resolve_inputs(0.14, None, None, None))
_ensure_planned_initial(root, RADIUS, land)
expected = [(v.x, v.y) for v in read_planned_route_csv(expected_route_csv_path(root)).vertices]
result = generate_initial_route(
expected,
terrain,
streams,
mode="follow",
weights=None,
max_grade=0.14,
min_radius_m=RADIUS,
with_profile=False,
)
saved = [
(v.x, v.y)
for v in read_planned_route_csv(_nodes_path(planned_route_initial_path(root))).vertices
]
nodes = [(node["x"], node["y"]) for node in result["nodes"]]
assert len(nodes) == len(saved)
assert all(abs(a[0] - b[0]) <= 1e-3 and abs(a[1] - b[1]) <= 1e-3 for a, b in zip(nodes, saved))
misses = [v for v in result["metrics"]["violations"] if v["kind"] == "station"]
assert len(misses) == result["metrics"]["over_limit"]["station"] == result["off_station_curves"]
assert "unsnapped" not in result # 측점 맞춤 필수 — 맞추기 전 선 안 냄(419)
def test_old_copy_flag_and_line_unchanged(tmp_path: Path) -> None:
"""옛 사본 — 초기 노선(깃발 칸 없음)이 이미 있으면 다시 안 씀 · 스냅 끔 그대로."""
root = _copy_expected(tmp_path)
initial = planned_route_initial_path(root)
points = [(v.x, v.y) for v in read_planned_route_csv(expected_route_csv_path(root)).vertices]
_write_planned_polyline(initial, points, RADIUS) # 옛 규칙(맞춤 없음 · 칸 없음)
before = {p: p.read_bytes() for p in (initial, _curves_path(initial), _nodes_path(initial))}
assert _ensure_planned_initial(root, RADIUS) is None
assert {p: p.read_bytes() for p in before} == before
assert load_station_snap(root) is False