"""455 (433-13 ②) 폴백 첫 종단 직선화 — 관 정착 · 직선 분할도 R > 500 m 꼭짓점 없음. 까닭: 초기값 직선화(438)가 1순위 지반 추종 선형에만 걸려, 1순위가 실패해 2 · 3순위(관 정착 · 직선 분할)로 내려가면 R > 500 m 꼭짓점이 남음. 사용자 확정(269-2) — 직선화 높이 한도 없음. """ from __future__ import annotations import math import sys from pathlib import Path import numpy as np import pytest HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B05_Profile.B05_Profile_Engine_Grade import GradeDesignOptions # noqa: E402 from B05_Profile.B05_Profile_Engine_Grade_Alignment import ( # noqa: E402 AlignmentPolicy, build_curves, is_straight_bend, ) from B05_Profile.B05_Profile_Engine_Grade_CurveLaw import flat_bends # noqa: E402 from B05_Profile.B05_Profile_Engine_Grade_Profile import ( # noqa: E402 design_alignment_profile, design_pipe_anchored_profile, ) PIPES = [60.0, 125.0, 190.0, 260.0, 330.0] def _ground(s: float) -> float: return 100.0 + 0.06 * s + 2.5 * math.sin(s / 45.0) + 0.05 * math.sin(s / 6.0) def _longitudinal() -> dict: samples = [ {"chainage_m": float(s), "elevation_m": round(_ground(s), 6), "valid": True} for s in range(0, 401) ] stations = [{"chainage_m": float(s), "kind": "regular"} for s in range(0, 401, 20)] return {"samples": samples, "stations": stations} OPTIONS = GradeDesignOptions( max_grade_pct=14.0, max_reverse_grade_pct=5.0, min_vertical_radius_m=100.0, min_curve_length_m=20.0, min_tangent_length_m=20.0, vertical_curve_skip_delta_pct=0.5, design_speed_kph=20, ) def _flat(alignment: dict) -> tuple[list[float], int]: """(R > 500 m 꺾임 측점 · 직선 위로 옮긴 꼭짓점 수) — 438 시험과 같은 판정.""" s = np.array([n["chainage_m"] for n in alignment["base_pvi"]], dtype=np.float64) z = np.array([n["elevation_m"] for n in alignment["base_pvi"]], dtype=np.float64) policy = AlignmentPolicy.from_dict(alignment["policy"]) grades = np.diff(z) / np.diff(s) straight = np.zeros(len(s), dtype=bool) for i in range(1, len(s) - 1): straight[i] = is_straight_bend(grades[i] - grades[i - 1], s[i] - s[i - 1], s[i + 1] - s[i]) flat = flat_bends(s, z, policy.default_curve_length_m) & ~straight assert all(float(c["r_m"]) <= 500.0 for c in alignment["curves"]) return s[flat].tolist(), int(straight.sum()) def test_관_정착_선형_R500_넘는_꼭짓점_없음() -> None: alignment, _profile = design_pipe_anchored_profile(_longitudinal(), OPTIONS, PIPES) flat, straight = _flat(alignment) assert flat == [] and 0 < straight < len(PIPES) # 직선 위로 옮긴 관 꼭짓점은 자동 R 을 안 남김 · 남은 꺾임은 곡선이 관 자리를 지남 radii = alignment["edits"]["curve_radii"] kinks = {n["chainage_m"] for n in alignment["base_pvi"][1:-1]} assert set(map(float, radii)) <= kinks s = np.array([n["chainage_m"] for n in alignment["base_pvi"]], dtype=np.float64) z = np.array([n["elevation_m"] for n in alignment["base_pvi"]], dtype=np.float64) curves, _ = build_curves(s, z, AlignmentPolicy.from_dict(alignment["policy"]), radii) assert all(float(c["chainage_m"]) in kinks for c in curves) def test_직선_분할_선형_R500_넘는_꼭짓점_없음(monkeypatch) -> None: alignment, _profile = design_alignment_profile(_longitudinal(), OPTIONS) assert _flat(alignment)[0] == [] # 풀이가 R > 500 m 꺾임(100 m · 대수차 0.5 %)을 낸 꼴 — 직선 위로 · 200 m 꺾임은 그대로 import B05_Profile.B05_Profile_Engine_Grade_Profile as engine s = np.array([0.0, 100.0, 200.0, 400.0]) z = np.array([100.0, 103.0, 106.5, 124.5]) monkeypatch.setattr(engine, "station_breakpoints", lambda *_a, **_k: s) monkeypatch.setattr(engine, "solve_alignment_elevations", lambda *_a, **_k: (z.copy(), True)) alignment, _profile = engine.design_alignment_profile(_longitudinal(), OPTIONS) flat, straight = _flat(alignment) assert flat == [] and straight == 1 nodes = {n["chainage_m"]: n["elevation_m"] for n in alignment["base_pvi"]} assert nodes[100.0] == pytest.approx(103.25) assert nodes[200.0] == pytest.approx(106.5) def test_부르는_곳() -> None: src = (ROOT / "B05_Profile" / "B05_Profile_Engine_Grade_Profile.py").read_text(encoding="utf-8") assert src.count("flat_bends(base_s, ") >= 3 # 지반 추종(straight_pass_through 안) 밖 둘 def test_여유로_들린_관_꼭짓점은_그대로() -> None: """관 여유 강제(+1.5 m)로 들린 관 꼭짓점 = 직선화 경계 · 여유 없는 관 꼭짓점만 직선 위.""" clearances = {125.0: 1.5} alignment, _profile = design_pipe_anchored_profile( _longitudinal(), OPTIONS, PIPES, pipe_clearances=clearances ) flat, straight = _flat(alignment) assert straight > 0 assert flat in ([], [125.0]) # 들린 꼭짓점만 R > 500 이 남을 수 있음 rows = {r["chainage_m"]: r["plan_elevation_m"] for r in alignment["stations"]} plan = np.interp(125.0, list(rows), list(rows.values())) assert plan > _ground(125.0) + 1.0