확폭을 측점마다 앞뒤 10m 세 점의 외접원 반경으로 다시 재던 것을 계획노선 곡선표 (시·종점·설계 반경)로 바꿈. 종전에는 같은 곡선 안에서도 측점마다 확폭이 갈리고, 곡선 밖 직선까지 확폭이 흘러나가며, 측점 간격보다 짧은 곡선은 통째로 빠졌음. 곡선 안은 설계 반경의 표값 한 값, 앞뒤 10m 는 0 으로 잇고, 그 밖은 확폭 없음. 곡선표가 없는 옛 프로젝트는 종전 방식으로 물러섬. 횡단도는 차도 끝에만 눈금이 있어 노면이 넓어져도 차도가 는 것인지 노견이 는 것인지 가릴 수 없었음. 노면 끝에도 옅은 눈금을 세우고 노폭 라벨에 노견 폭을 덧붙임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
212 lines
8.6 KiB
Python
212 lines
8.6 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]
|
||
|
||
|
||
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
|