123 lines
5.2 KiB
Python
123 lines
5.2 KiB
Python
"""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
|