⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt` 가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는 시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음. - `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침** - `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시) - 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q` 옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0 ⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
151 lines
5.7 KiB
Python
151 lines
5.7 KiB
Python
"""곡선부 확폭 — 표·경계·편측 적용·상한을 확인한다(2026-09-06).
|
||
|
||
파이썬과 브라우저가 한 세트이므로 같은 기대값을 `test_b06_curve_widening.mjs` 도 쓴다.
|
||
값이 갈리면 두 화면이 다른 단면을 그린다.
|
||
"""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
import numpy as np # noqa: E402
|
||
|
||
from B05_Profile.B05_Profile_Engine_Sections_Core import _plan_radii # noqa: E402
|
||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||
from config.config_system_design import curve_widening_m # noqa: E402
|
||
|
||
# (반경 m, 기대 확폭 m) — 별표2 Ⅰ.2.나.(4). 경계는 "이상 ~ 미만".
|
||
TABLE_CASES = [
|
||
(9.9, 0.0),
|
||
(10.0, 2.25),
|
||
(12.999, 2.25),
|
||
(13.0, 2.0),
|
||
(14.0, 1.75),
|
||
(15.0, 1.5),
|
||
(18.0, 1.25),
|
||
(20.0, 1.0),
|
||
(25.0, 0.75),
|
||
(30.0, 0.5),
|
||
(40.0, 0.25),
|
||
(45.0, 0.0),
|
||
(200.0, 0.0),
|
||
]
|
||
|
||
|
||
def _flat_samples() -> list[dict]:
|
||
return [{"offset_m": float(o), "elevation_m": 100.0, "valid": True} for o in range(-25, 26)]
|
||
|
||
|
||
def test_widening_table_boundaries() -> None:
|
||
for radius, expected in TABLE_CASES:
|
||
assert curve_widening_m(radius) == expected, radius
|
||
assert curve_widening_m(None) == 0.0
|
||
|
||
|
||
def test_widening_applies_to_outer_side_only() -> None:
|
||
samples = _flat_samples()
|
||
base = compute_cross_design(samples, 100.0, ground_type="soil", section_mode="both_fill")
|
||
right = compute_cross_design(
|
||
samples,
|
||
100.0,
|
||
ground_type="soil",
|
||
section_mode="both_fill",
|
||
plan_radius_m=16.0,
|
||
curve_outer_side="right",
|
||
)
|
||
left = compute_cross_design(
|
||
samples,
|
||
100.0,
|
||
ground_type="soil",
|
||
section_mode="both_fill",
|
||
plan_radius_m=16.0,
|
||
curve_outer_side="left",
|
||
)
|
||
assert base["widening_left_m"] == 0.0 and base["widening_right_m"] == 0.0
|
||
assert right["widening_right_m"] == 1.5 and right["widening_left_m"] == 0.0
|
||
assert left["widening_left_m"] == 1.5 and left["widening_right_m"] == 0.0
|
||
# 확폭은 붙은 쪽 차도 끝만 밖으로 민다.
|
||
assert right["carriageway_edges"]["left"] == base["carriageway_edges"]["left"]
|
||
assert right["carriageway_edges"]["right"]["offset_m"] < base["carriageway_edges"]["right"]["offset_m"]
|
||
assert right["carriageway_width_m"] == base["carriageway_width_m"] + 1.5
|
||
|
||
|
||
def test_widening_capped_by_legal_max_width() -> None:
|
||
"""확폭을 더한 유효너비는 5m 를 넘지 않는다(별표2 — 최대 5미터까지)."""
|
||
samples = _flat_samples()
|
||
design = compute_cross_design(
|
||
samples,
|
||
100.0,
|
||
ground_type="soil",
|
||
section_mode="both_fill",
|
||
plan_radius_m=11.0, # 표값 2.25m
|
||
curve_outer_side="right",
|
||
)
|
||
assert design["carriageway_width_m"] <= 5.0 + 1e-9
|
||
# 규격 3.0m 이면 2.0m 까지만 붙는다.
|
||
assert design["widening_right_m"] == 2.0
|
||
|
||
|
||
def test_plan_radius_and_outer_side_from_polyline() -> None:
|
||
"""반지름 50m 원호에서 반경 50m 가 나오고, 좌회전이면 바깥은 우측이다."""
|
||
angles = np.linspace(0.0, np.pi / 2, 400)
|
||
radius = 50.0
|
||
left_turn = np.column_stack([radius * np.cos(angles), radius * np.sin(angles)])
|
||
chainage = np.r_[
|
||
0.0, np.cumsum(np.hypot(np.diff(left_turn[:, 0]), np.diff(left_turn[:, 1])))
|
||
]
|
||
radii, sides = _plan_radii(left_turn, chainage, np.array([40.0]), float(chainage[-1]))
|
||
assert abs(radii[0] - radius) < 0.1
|
||
assert sides[0] == "right"
|
||
|
||
right_turn = np.column_stack([radius * np.cos(-angles), radius * np.sin(-angles)])
|
||
chainage2 = np.r_[
|
||
0.0, np.cumsum(np.hypot(np.diff(right_turn[:, 0]), np.diff(right_turn[:, 1])))
|
||
]
|
||
_, sides2 = _plan_radii(right_turn, chainage2, np.array([40.0]), float(chainage2[-1]))
|
||
assert sides2[0] == "left"
|
||
|
||
|
||
def test_straight_route_has_no_radius() -> None:
|
||
line = np.array([[0.0, 0.0], [300.0, 0.0]])
|
||
radii, sides = _plan_radii(line, np.array([0.0, 300.0]), np.array([50.0, 150.0]), 300.0)
|
||
assert radii == [None, None]
|
||
assert sides == [None, None]
|
||
|
||
|
||
def test_widening_taper_runs_before_and_after_curve() -> None:
|
||
"""곡선 앞뒤 10m 안 측점은 확폭이 0 → W 로 이어진다(2026-09-06)."""
|
||
from B05_Profile.B05_Profile_Engine_Sections_Core import _curve_widenings
|
||
|
||
# 5m 간격 측점: 20~30m 구간만 곡선(R=16 → 1.5m), 나머지는 직선.
|
||
chainage = np.arange(0.0, 55.0, 5.0)
|
||
radii: list[float | None] = [None] * len(chainage)
|
||
sides: list[str | None] = [None] * len(chainage)
|
||
for index, value in enumerate(chainage):
|
||
if 20.0 <= value <= 30.0:
|
||
radii[index] = 16.0
|
||
sides[index] = "right"
|
||
widenings, out_sides = _curve_widenings(chainage, radii, sides)
|
||
by_chainage = dict(zip(chainage.tolist(), widenings, strict=True))
|
||
# 곡선 안은 표값 그대로.
|
||
assert by_chainage[20.0] == 1.5 and by_chainage[25.0] == 1.5 and by_chainage[30.0] == 1.5
|
||
# 앞뒤 10m 는 선형으로 줄어든다 — 5m 지점에서 절반.
|
||
assert by_chainage[15.0] == 0.75 and by_chainage[35.0] == 0.75
|
||
assert by_chainage[10.0] == 0.0 and by_chainage[40.0] == 0.0
|
||
# 테이퍼 측점도 확폭이 붙는 쪽을 물려받는다.
|
||
assert out_sides[chainage.tolist().index(15.0)] == "right"
|
||
# 테이퍼 밖은 방향이 없다.
|
||
assert out_sides[chainage.tolist().index(5.0)] is None
|
||
|
||
|
||
def test_widening_without_curve_stays_zero() -> None:
|
||
from B05_Profile.B05_Profile_Engine_Sections_Core import _curve_widenings
|
||
|
||
chainage = np.arange(0.0, 60.0, 20.0)
|
||
widenings, sides = _curve_widenings(chainage, [None] * 3, [None] * 3)
|
||
assert widenings == [0.0, 0.0, 0.0]
|
||
assert sides == [None, None, None]
|