Files
Aislo/resources/tester/test_b06_curve_widening.py
T
eomsangdonandClaude Opus 5 bd770fc351 fix(b06): 곡선부 확폭을 설계 곡선표 기준으로 바로잡고 횡단도에 노견 표시 추가
확폭을 측점마다 앞뒤 10m 세 점의 외접원 반경으로 다시 재던 것을 계획노선 곡선표
(시·종점·설계 반경)로 바꿈. 종전에는 같은 곡선 안에서도 측점마다 확폭이 갈리고,
곡선 밖 직선까지 확폭이 흘러나가며, 측점 간격보다 짧은 곡선은 통째로 빠졌음.
곡선 안은 설계 반경의 표값 한 값, 앞뒤 10m 는 0 으로 잇고, 그 밖은 확폭 없음.
곡선표가 없는 옛 프로젝트는 종전 방식으로 물러섬.

횡단도는 차도 끝에만 눈금이 있어 노면이 넓어져도 차도가 는 것인지 노견이 는 것인지
가릴 수 없었음. 노면 끝에도 옅은 눈금을 세우고 노폭 라벨에 노견 폭을 덧붙임.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
2026-09-12 14:26:40 +09:00

212 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""곡선부 확폭 — 표·경계·편측 적용·상한을 확인한다(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