Files
Aislo/resources/tester/test_351_2_curve_stations.py
T

178 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""351-2 추천 노선 곡선 MC → 일반측점 — `B05_Profile_Engine_RouteInitial_Station`.
./venv/Scripts/python.exe -m pytest resources/tester/test_351_2_curve_stations.py -q
모든 곡선 MC % 20 ≈ 0 · 이웃 MC 20 m 이상 · 곡선 수 ≤ 내부 일반측점 수 · 맞추기 전 선에서
`ROUTE_STATION_SNAP_MAX_GAP_M`(6 m) 안 · 화면이 펴서 다시 짜도(저장 반올림) 그대로 · 추천 결과는
측점에 맞춘 선만(맞추기 전 선 안 냄 · 419) · 합성 예상노선 8벌 수치.
"""
import math
import random
import numpy as np
import pytest
from shapely.geometry import LineString
from B05_Profile.B05_Profile_Engine_RouteInitial import Terrain, generate_initial_route
from B05_Profile.B05_Profile_Engine_RouteInitial_Radius import _flat
from B05_Profile.B05_Profile_Engine_RouteInitial_Station import (
MC_TOL_M,
STATION_M,
curve_mc_chainages,
off_station_count,
snap_curves_to_stations,
station_hits,
)
from common_util.common_util_route_polyline import build_planned_polyline
from config.config_system_design import ROUTE_STATION_SNAP_MAX_GAP_M
def _ls(vertices: list) -> LineString:
"""겹친 정점 뺀 선(길이 0 조각은 GEOS 경고)."""
return LineString([xy for i, xy in enumerate(vertices) if not i or xy != vertices[i - 1]])
def _zigzag(seed: int) -> list[tuple[float, float]]:
rng = random.Random(seed)
points, heading = [(0.0, 0.0)], 0.0
for _ in range(12):
heading += rng.uniform(-0.8, 0.8)
step = rng.uniform(30.0, 90.0)
x, y = points[-1]
points.append((x + step * math.cos(heading), y + step * math.sin(heading)))
return points
def _check(outline, before) -> None:
mcs = curve_mc_chainages(outline)
assert outline.curves and off_station_count(outline) == 0
for mc in mcs:
assert abs(mc - STATION_M * round(mc / STATION_M)) <= MC_TOL_M and mc >= STATION_M - 1
assert all(b - a >= STATION_M - 2 * MC_TOL_M for a, b in zip(mcs, mcs[1:]))
length = _ls(outline.vertices).length
assert len(outline.curves) <= int((length - 1e-6) // STATION_M) # 내부 일반측점 수
gap = _ls(outline.vertices).hausdorff_distance(_ls(before.vertices))
assert gap <= ROUTE_STATION_SNAP_MAX_GAP_M + 0.05
# 시점 · 종점 그대로
assert outline.vertices[0] == pytest.approx(before.vertices[0], abs=1e-3)
assert outline.vertices[-1] == pytest.approx(before.vertices[-1], abs=1e-3)
def _reflattened(outline, min_radius: float):
"""화면 `flattenServerPlan` → [확인] 다시 짜기와 같은 꼴(저장 반올림: IP 0.1 mm · R 1 mm)."""
points, flags, radii = _flat(outline)
return build_planned_polyline(
[(round(x, 4), round(y, 4)) for x, y in points],
min_radius_m=min_radius,
simplify=False,
curve_flags=flags,
radii=[None if r is None else round(r, 3) for r in radii],
)
@pytest.mark.parametrize("seed", range(8))
def test_zigzag_mc_on_stations(seed: int) -> None:
before = build_planned_polyline(
_zigzag(seed), min_radius_m=15.0, simplify=False, curve_flags=[True] * 13
)
after = snap_curves_to_stations(before, 15.0)
assert after is not None
_check(after, before)
for curve in after.curves:
assert curve.radius_m >= 15.0 - 1e-6 or curve.violations
# 저장 뒤 다시 짜도 MC 그대로
again = _reflattened(after, 15.0)
assert curve_mc_chainages(again) == pytest.approx(curve_mc_chainages(after), abs=MC_TOL_M)
def test_no_curves_unchanged() -> None:
straight = build_planned_polyline([(0.0, 0.0), (100.0, 0.0)], min_radius_m=12.0)
assert snap_curves_to_stations(straight, 12.0) is straight
def test_too_close_curves_merge_or_drop() -> None:
"""20 m 안에 꺾임 셋 — 곡선 수 ≤ 내부 일반측점 수로 합쳐짐."""
points = [(0.0, 0.0), (40.0, 0.0), (48.0, 4.0), (54.0, 10.0), (60.0, 20.0), (60.0, 80.0)]
before = build_planned_polyline(
points, min_radius_m=12.0, simplify=False, curve_flags=[True] * 6
)
after = snap_curves_to_stations(before, 12.0)
assert after is not None and station_hits(after) == len(after.curves)
assert len(after.curves) < len(before.curves)
def _terrain() -> Terrain:
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)
return Terrain(x, y, z, np.ones(xx.shape, dtype=bool))
@pytest.fixture(scope="module")
def hill_runs() -> dict:
expected = [(float(x), 200.0 + 40.0 * math.sin(x / 60.0)) for x in np.arange(50.0, 752.0, 4.0)]
return {
mode: generate_initial_route(
expected, _terrain(), [], mode=mode, weights=None, max_grade=0.14, min_radius_m=12.0
)
for mode in ("follow", "gentle", "earthwork", "weighted")
}
@pytest.mark.parametrize("mode", ["follow", "gentle", "earthwork", "weighted"])
def test_recommend_result_on_stations(hill_runs: dict, mode: str) -> None:
result = hill_runs[mode]
assert result["off_station_curves"] == 0
outline = build_planned_polyline(
[tuple(n[k] for k in ("x", "y")) for n in result["nodes"]],
min_radius_m=12.0,
simplify=False,
curve_flags=[n["radius_m"] is not None for n in result["nodes"]],
radii=[n["radius_m"] for n in result["nodes"]],
)
# 화면이 펴서 다시 짠 선 = 서버 선
assert np.allclose(outline.vertices, result["planned"], atol=1e-3)
assert station_hits(outline) == len(outline.curves) > 0
if mode != "follow": # 갈래는 기울기 상한 안(언덕은 비켜 갈 수 있음)
assert result["metrics"]["over_limit"]["total"] == 0
assert result["metrics"]["max_grade_pct"] <= 14.0 + 1e-6
# 측점 맞춤 필수 — 맞추기 전 선은 안 냄(419)
assert "unsnapped" not in result
def test_합성_지형_modes_before_after(hill_runs: dict) -> None:
"""합성 언덕(14 % · R12) — 갈래별 최대 기울기와 측점 밖 곡선을 함께 지킨다."""
on_total, curves_total = 0, 0
for mode, result in hill_runs.items():
after = result["metrics"]
curves = len(result["curves"])
on_total += curves - result["off_station_curves"]
curves_total += curves
print(
f"{mode}: 넘은 곳 {after['over_limit']['total']}"
f" · 최대 기울기 {after['max_grade_pct']:.1f} %"
f" · 곡선 {curves}(측점 밖 {result['off_station_curves']})"
)
print(f"측점 위 곡선 {on_total}/{curves_total} = {100 * on_total / curves_total:.0f}%")
assert curves_total and on_total == curves_total
def test_합성_예상노선_eight_outlines() -> None:
"""합성 예상노선 — R 12 · 15 · 20 · 30 × 두 선 = 8벌 측점 맞춤."""
points = [(float(x), 200.0 + 40.0 * math.sin(x / 60.0)) for x in np.arange(50.0, 752.0, 20.0)]
shifted = [(x, y + 3.0 * math.sin(x / 35.0)) for x, y in points]
full, hits, total = 0, 0, 0
for radius in (12.0, 15.0, 20.0, 30.0):
for line in (points, shifted):
before = build_planned_polyline(line, min_radius_m=radius)
after = snap_curves_to_stations(before, radius)
assert after is not None
on = station_hits(after)
full += on == len(after.curves)
hits, total = hits + on, total + len(after.curves)
gap = _ls(after.vertices).hausdorff_distance(_ls(before.vertices))
assert gap <= ROUTE_STATION_SNAP_MAX_GAP_M + 0.05
print(f"합성 8벌 중 {full}벌 전부 · 곡선 {hits}/{total} = {100 * hits / total:.0f}% 측점 위")
assert full >= 7 and hits / total >= 0.95