"""곡선부 확폭 — 표·경계·편측 적용·상한을 확인한다(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] def test_design_curve_spans_from_curve_table() -> None: """설계 곡선표의 시·종점이 노선 누가거리로 바뀌고, 회전 방향으로 바깥쪽이 갈린다.""" from B05_Profile.B05_Profile_Engine_Sections_Core import _design_curve_spans # ㄱ자 노선 — (0,0) → (100,0) → (100,100). 교점 (100,0) 에서 좌회전. line = np.array([[0.0, 0.0, 0.0], [100.0, 0.0, 0.0], [100.0, 100.0, 0.0]]) chainage = np.array([0.0, 100.0, 200.0]) curves = [ { "start": [90.0, 0.0], "apex": [100.0, 0.0], "end": [100.0, 10.0], "radius_m": 12.0, } ] spans = _design_curve_spans(line, chainage, curves) assert len(spans) == 1 start, end, radius, side = spans[0] assert abs(start - 90.0) < 1e-6 and abs(end - 110.0) < 1e-6 assert radius == 12.0 # 좌회전이면 안쪽이 좌측이라 바깥은 우측이다. assert side == "right" # 노선에서 멀리 떨어진 곡선은 버린다. assert _design_curve_spans(line, chainage, [{**curves[0], "start": [90.0, 500.0]}]) == [] def test_design_curve_widening_is_one_value_inside_the_curve() -> None: """같은 곡선 안 측점은 설계 반경의 표값 하나를 쓰고, 앞뒤 10m 만 이어 준다.""" from B05_Profile.B05_Profile_Engine_Sections_Core import _design_curve_widenings # 곡선 100~140m, 설계 반경 16m(표값 1.5m). 측점 5m 간격. spans = [(100.0, 140.0, 16.0, "left")] chainage = np.arange(80.0, 165.0, 5.0) radii, sides, widenings = _design_curve_widenings(chainage, spans) table = dict(zip(chainage.tolist(), widenings, strict=True)) # 곡선 안은 어디서나 같은 값 — 종전에는 측점마다 반경을 다시 재 값이 갈렸다. for station in (100.0, 110.0, 120.0, 130.0, 140.0): assert table[station] == 1.5, station # 앞뒤 10m 는 0 으로 잇는다. assert table[95.0] == 0.75 and table[145.0] == 0.75 assert table[90.0] == 0.0 and table[150.0] == 0.0 # 반경은 곡선 안에서만 남고, 테이퍼·직선 자리는 비어 있다. by_index = dict(zip(chainage.tolist(), radii, strict=True)) assert by_index[120.0] == 16.0 assert by_index[95.0] is None and by_index[90.0] is None # 방향은 곡선 것을 따라간다. assert sides[chainage.tolist().index(95.0)] == "left" def test_design_curve_widening_takes_the_wider_of_overlapping_curves() -> None: """곡선이 겹치거나 붙어 있으면 확폭이 큰 쪽을 따른다.""" from B05_Profile.B05_Profile_Engine_Sections_Core import _design_curve_widenings spans = [(100.0, 120.0, 35.0, "left"), (118.0, 140.0, 12.0, "right")] chainage = np.array([110.0, 119.0, 130.0]) _, sides, widenings = _design_curve_widenings(chainage, spans) assert widenings[0] == 0.5 # R=35 → 0.5m assert widenings[1] == 2.25 and sides[1] == "right" # 겹친 자리는 급한 곡선 값 assert widenings[2] == 2.25