Files
Aislo/resources/tester/test_224_4_recommend_radius.py
T

157 lines
6.3 KiB
Python

"""추천 노선 큰 R(PLAN 328 · 요구 224-4) — 1단 R 맞춤 · 2단 허용 이탈 안 가장 큰 R.
./venv/Scripts/python.exe -m pytest resources/tester/test_224_4_recommend_radius.py -q
· 네 갈래(영선 · 토공 최소 · 기울기 최소 · 비중) R ≥ 최소(모자라면 위반) · 하나 이상 큼.
· 위반 ≤ 같은 노드로 짠 최소 R 판(노드마다 곡선 · 옛 규칙) — 묶음의 잰 노선 전부.
· 예상노선 추적은 전처리 초기값 그대로.
· 2단 = 탐색 경로 이탈 ≤ 허용(4 m) · 1 m 더 키우면 넘음 · [확인] 재구성 뒤 같은 R.
"""
import math
import numpy as np
import pytest
from shapely.geometry import LineString, Point
import B05_Profile.B05_Profile_Engine_RouteInitial as engine
from B05_Profile.B05_Profile_Engine_RouteInitial import Terrain, generate_initial_route
from B05_Profile.B05_Profile_Engine_RouteInitial_Radius import _flat, widen
from common_util.common_util_route_polyline import build_planned_polyline
from config.config_system_design import ROUTE_RECOMMEND_MAX_GAP_M
GRADE = 0.14
RADIUS = 12.0
SHOWN = ["gradeline", "earthwork", "gentle", "weighted"]
def _line() -> list[tuple[float, float]]:
return [(float(x), 200.0) for x in np.arange(50.0, 752.0, 4.0)]
@pytest.fixture(scope="module")
def runs() -> dict:
"""가운데 언덕(40 m) — 곧은 예상노선이 꼭대기를 넘어 갈래마다 돌아감. 묶음 · 잼도 붙잡음."""
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))
caught: dict = {}
real = engine.build_pool
def spy(graph, follow, max_grade, min_radius_m, measure):
caught["measure"] = measure
caught["pool"] = real(graph, follow, max_grade, min_radius_m, measure)
return caught["pool"]
engine._POOLS.clear()
engine.build_pool = spy
try:
results = {
mode: generate_initial_route(
_line(), terrain, [], mode=mode, weights=None, max_grade=GRADE, min_radius_m=RADIUS
)
for mode in [*SHOWN, "follow"]
}
finally:
engine.build_pool = real
engine._POOLS.clear()
return {"results": results, **caught}
def test_shown_branches_radius_at_least_min_and_some_larger(runs: dict) -> None:
radii = []
for mode in SHOWN:
for curve in runs["results"][mode]["curves"]:
assert curve["radius_m"] >= RADIUS - 1e-6 or curve["violations"], mode
radii.append(curve["radius_m"])
assert max(radii) > RADIUS + 1.0
def test_violations_not_above_min_radius_version(runs: dict) -> None:
measure = runs["measure"]
for (nodes, kw), (_, metrics) in runs["pool"].traced.items():
floor = build_planned_polyline(
list(nodes), min_radius_m=RADIUS, simplify=False, curve_flags=[True] * len(nodes)
)
least = measure(floor, **dict(kw))["over_limit"]["total"]
assert metrics["over_limit"]["total"] <= least
def test_follow_unchanged(runs: dict) -> None:
initial = build_planned_polyline(_line(), min_radius_m=RADIUS)
result = runs["results"]["follow"]
assert result["planned"] == [[round(x, 4), round(y, 4)] for x, y in initial.vertices]
assert result["curves"] == [curve.as_dict() for curve in initial.curves]
def _bend(radius: float) -> list[tuple[float, float]]:
"""직선 100 m → 반지름 `radius` 원호 90° → 직선 100 m 를 2 m 간격 점으로."""
points = [(float(x), 0.0) for x in np.arange(-100.0, 0.0, 2.0)]
steps = int(radius * math.pi / 2 / 2.0)
points += [
(radius * math.sin(a), radius - radius * math.cos(a))
for a in np.linspace(0.0, math.pi / 2, steps)
]
points += [(radius, radius + float(y)) for y in np.arange(2.0, 102.0, 2.0)]
return points
def _window_gap(path, outline, curve) -> float:
"""곡선 앞뒤 직선 가운데점 사이 경로 점이 노선에서 가장 먼 거리."""
line = LineString(path)
drawn = LineString(outline.vertices)
apex = curve.apex
nodes = [(node.x, node.y) for node in outline.nodes]
at = nodes.index(min(nodes, key=lambda n: math.dist(n, apex)))
ends = [
line.project(Point((apex[0] + n[0]) / 2, (apex[1] + n[1]) / 2))
for n in (nodes[at - 1], nodes[at + 1])
]
return max(
drawn.distance(Point(p)) for p in path if min(ends) <= line.project(Point(p)) <= max(ends)
)
def test_widen_largest_radius_within_gap() -> None:
path = _bend(80.0)
fitted = build_planned_polyline(path, min_radius_m=RADIUS)
wide = widen(fitted, path, RADIUS)
assert len(wide.curves) == 1
curve = wide.curves[0]
assert curve.radius_m >= fitted.curves[0].radius_m
assert curve.radius_m == math.floor(curve.radius_m) # 1 m 내림
assert curve.radius_m > 40.0
assert _window_gap(path, wide, curve) <= ROUTE_RECOMMEND_MAX_GAP_M + 1e-6
# 1 m 더 키우면 허용 이탈을 넘음(가장 큰 값) — 셈 자(shapely)로 따로 잼
points, flags, radii = _flat(wide)
seat = flags.index(True)
radii[seat] = curve.radius_m + 1.0
bigger = build_planned_polyline(
points, min_radius_m=RADIUS, simplify=False, curve_flags=flags, radii=radii
)
assert _window_gap(path, bigger, bigger.curves[0]) > ROUTE_RECOMMEND_MAX_GAP_M
def test_widen_survives_confirm_rebuild() -> None:
"""화면이 펴서 보내는 꼴(교각점 · 곡선 켬 · R)로 [확인]이 다시 짜도 같은 R."""
path = _bend(60.0) + [(60.0 + float(x), 160.0) for x in np.arange(2.0, 120.0, 2.0)]
wide = widen(build_planned_polyline(path, min_radius_m=RADIUS), path, RADIUS)
points, flags, radii = _flat(wide)
again = build_planned_polyline(
points, min_radius_m=RADIUS, simplify=False, curve_flags=flags, radii=radii
)
assert [c.radius_m for c in again.curves] == [c.radius_m for c in wide.curves]
assert again.vertices == wide.vertices
def test_widen_keeps_fitted_when_gap_already_over() -> None:
"""허용 이탈 0 이면 키울 R 이 없어 맞춘 R 그대로."""
path = _bend(80.0)
fitted = build_planned_polyline(path, min_radius_m=RADIUS)
same = widen(fitted, path, RADIUS, max_gap_m=0.0)
assert [round(c.radius_m, 2) for c in same.curves] == [
round(c.radius_m, 2) for c in fitted.curves
]