fix(B05): 노선 추천 균형 종단을 본 설계 횡단과 맞춤 — 곡선표 확폭 · 비탈 안 닫힘 넓힘 · 암반 경계 · 누가토량 ±500㎥(78)
- 확폭은 노선 곡선표로(후보 · 편집 띠 · [확인]) · 없으면 종전 실측 반경 - 반폭에서 비탈이 안 닫히면 지표면 끝까지 넓힌 샘플로 다시 셈(본 설계 extend_unclosed 와 같은 뜻) - 암반 경계 기본 오프셋(본 설계 기본 설계와 같음) - 균형 목표 = 누가토량 끝값(절토 다짐 − 성토) ±500㎥ · 고른 선은 보간 말고 바로 셈해 λ 를 더 좁힘 - 후보 줄 세우기는 보간값 그대로(빠름) · 화면에 낼 선만 바로 셈 - 추천 창 한 줄: 불균형 % → 누가토량 ±㎥ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EMfGERKUg5nMMNQsyVZ2j
This commit is contained in:
@@ -490,7 +490,8 @@ const withProfile = model.normalizeRouteInitial(
|
||||
borrow_m3: 0,
|
||||
spoil_m3: 39.9,
|
||||
imbalance_pct: -28.6,
|
||||
tolerance_pct: 10,
|
||||
mass_end_m3: -39.9,
|
||||
mass_tolerance_m3: 500,
|
||||
max_grade_pct: 7.5,
|
||||
grade_limit_pct: 14,
|
||||
curve_over: 1,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_63_4_route_profile.py -q
|
||||
|
||||
합성 지형(노선 따라 ±20 % 물결 · 옆경사 50 %)에서 균형 계획선이 종단기울기 상한을 지키고,
|
||||
절(다짐 환산) · 성토가 허용(10 %) 안에서 맞고, 시 · 종점은 지반고에 붙는다. 토공 최소 갈래는
|
||||
누가토량 끝값(절 다짐 − 성)이 ±500㎥ 안에서 맞고, 시 · 종점은 지반고에 붙는다. 토공 최소 갈래는
|
||||
균형 종단 토공(절 + 성 + 반입 + 사토)으로 고른다. 균형 종단은 화면 캐시에 안 적힌다.
|
||||
"""
|
||||
|
||||
@@ -56,7 +56,11 @@ def test_상한을_지키고_지반_기울기는_넘는다(profile: dict) -> Non
|
||||
|
||||
def test_절성토_균형은_허용_안(profile: dict) -> None:
|
||||
assert profile["balanced"] is True
|
||||
assert abs(profile["imbalance_pct"]) <= profile["tolerance_pct"]
|
||||
# 누가토량 끝값(절토 다짐 − 성토)이 ±500㎥ 안(PLAN 78)
|
||||
assert profile["mass_tolerance_m3"] == 500.0
|
||||
assert abs(profile["mass_end_m3"]) <= profile["mass_tolerance_m3"]
|
||||
net = profile["cut_compacted_m3"] - profile["fill_m3"]
|
||||
assert profile["mass_end_m3"] == pytest.approx(net, abs=0.2)
|
||||
# 반입 − 사토 = 성토 − 절토(다짐 환산)
|
||||
net = profile["fill_m3"] - profile["cut_compacted_m3"]
|
||||
assert abs((profile["borrow_m3"] - profile["spoil_m3"]) - net) <= 0.2
|
||||
@@ -114,5 +118,6 @@ def test_균형_종단은_캐시에_안_남는다() -> None:
|
||||
assert result["cached"]["metrics"]["cutM3"] is not None # 비교값은 그대로 캐시
|
||||
assert result["missing"] is None and result["broken"] is None
|
||||
assert "반입 0㎥" in result["line"] and "덮개 모자란 관 1/2" in result["line"]
|
||||
assert "누가토량 -40㎥(허용 ±500㎥)" in result["line"]
|
||||
assert result["svg"].count("<path") == 2
|
||||
assert '<th scope="row">토공 최소</th>' in result["table"]
|
||||
|
||||
@@ -24,8 +24,8 @@ def _strip_points(monkeypatch, tmp_path: Path, criteria=None) -> tuple[list, obj
|
||||
async def plan_criteria(_pid):
|
||||
return RADIUS_M, 0.0, 0.0
|
||||
|
||||
async def profile_of(_pid, _root, points, crit):
|
||||
seen["points"], seen["criteria"] = points, crit
|
||||
async def profile_of(_pid, _root, points, crit, curves=None):
|
||||
seen["points"], seen["criteria"], seen["curves"] = points, crit, curves
|
||||
return {"stations_m": [0.0, 20.0], "plan_m": [1.0, 2.0]}
|
||||
|
||||
monkeypatch.setattr(replan, "_project_paths", paths)
|
||||
@@ -60,6 +60,33 @@ def test_띠_노선점은_확인이_읽는_CSV와_같다(monkeypatch, tmp_path:
|
||||
assert len(points) > len(NODES) # 원호가 펴져 있다
|
||||
|
||||
|
||||
def test_띠는_그_노선의_곡선표로_확폭을_맨다(monkeypatch, tmp_path: Path) -> None:
|
||||
seen: dict = {}
|
||||
|
||||
async def paths(_pid):
|
||||
return tmp_path, "x"
|
||||
|
||||
async def plan_criteria(_pid):
|
||||
return RADIUS_M, 0.0, 0.0
|
||||
|
||||
async def profile_of(_pid, _root, _points, _crit, curves=None):
|
||||
seen["curves"] = curves
|
||||
return {"stations_m": [0.0, 20.0], "plan_m": [1.0, 2.0]}
|
||||
|
||||
monkeypatch.setattr(replan, "_project_paths", paths)
|
||||
monkeypatch.setattr(replan, "_plan_criteria", plan_criteria)
|
||||
monkeypatch.setattr(initial, "balanced_profile_of", profile_of)
|
||||
request = initial.ProfilePreviewRequest(
|
||||
vertices=[
|
||||
replan.RouteVertexInput(x=x, y=y, curve=i != 2, radius_m=40.0 if i == 1 else None)
|
||||
for i, (x, y) in enumerate(NODES)
|
||||
]
|
||||
)
|
||||
asyncio.run(initial.profile_preview(uuid4(), request))
|
||||
assert seen["curves"] and 40.0 in [c["radius_m"] for c in seen["curves"]]
|
||||
assert all({"start", "apex", "end", "radius_m"} <= set(c) for c in seen["curves"])
|
||||
|
||||
|
||||
def test_띠는_지금_추천_제약을_싣는다(monkeypatch, tmp_path: Path) -> None:
|
||||
criteria = replan.RouteInitialCriteria(max_grade_pct=12.0)
|
||||
_, got = _strip_points(monkeypatch, tmp_path, criteria)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""노선 추천 균형 종단 — 본 설계와 같은 횡단 · 누가토량 ±500㎥(PLAN 78).
|
||||
|
||||
랩탑 메인 노선 271: 추천은 곡선표 확폭 · 비탈 안 닫힘 넓힘을 빼 성토를 4,200㎥ 적게 봄.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import (
|
||||
CROSS_HALF_M,
|
||||
_station_areas,
|
||||
_widenings,
|
||||
route_profile,
|
||||
route_sections,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Search import Terrain
|
||||
from config.config_system_design import curve_widening_m
|
||||
|
||||
|
||||
def _steep() -> Terrain:
|
||||
# 노선(y = 200) 왼쪽(+y)으로 70 % 내리막 — 성토 비탈(1:1.5)이 ±20 m 안에서 안 닫힘
|
||||
x = np.arange(0.0, 202.0, 2.0)
|
||||
y = np.arange(120.0, 282.0, 2.0)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
z = 100.0 - 0.7 * (yy - 200.0) + 0.02 * xx
|
||||
return Terrain(x, y, z, np.ones(xx.shape, dtype=bool))
|
||||
|
||||
|
||||
def test_곡선표가_있으면_그_반경으로_확폭():
|
||||
xy = np.array([(0.0, 0.0), (40.0, 0.0), (80.0, 0.0)])
|
||||
chain = np.array([0.0, 40.0, 80.0])
|
||||
at = np.array([0.0, 20.0, 40.0, 60.0, 80.0])
|
||||
table = [{"start": [30.0, 0.0], "apex": [40.0, 1.0], "end": [50.0, 0.0], "radius_m": 12.0}]
|
||||
rows = _widenings(xy, chain, at, table)
|
||||
assert rows[2]["curve_widening_m"] == curve_widening_m(12.0)
|
||||
assert rows[2]["plan_radius_m"] == 12.0
|
||||
# 곡선표 없으면 직선(반경 실측 없음) — 확폭 0
|
||||
assert all(row["curve_widening_m"] == 0 for row in _widenings(xy, chain, at))
|
||||
|
||||
|
||||
def test_비탈_안_닫히면_넓힌_샘플로_셈():
|
||||
at, ground, sections, modes, curves = route_sections([(20.0, 200.0), (180.0, 200.0)], _steep())
|
||||
i = len(at) // 2
|
||||
samples = sections[i]
|
||||
assert max(s["offset_m"] for s in samples) > CROSS_HALF_M # 반폭 밖까지 떠 둠
|
||||
narrow = [s for s in samples if abs(s["offset_m"]) <= CROSS_HALF_M]
|
||||
high = float(ground[i] + 3.0) # 쌓기 — 내리막 쪽 성토 비탈이 길어짐
|
||||
_, _, wide_fill = _station_areas(samples, modes[i], high, curves[i])
|
||||
_, _, narrow_fill = _station_areas(narrow, modes[i], high, curves[i])
|
||||
assert wide_fill > narrow_fill + 1.0
|
||||
|
||||
|
||||
def test_누가토량_끝값은_허용_안():
|
||||
profile = route_profile(
|
||||
[(20.0, 200.0), (180.0, 200.0)], _steep(), max_grade=0.14, design_speed=20
|
||||
)
|
||||
assert profile["exact"] is True
|
||||
assert abs(profile["mass_end_m3"]) <= profile["mass_tolerance_m3"] == 500.0
|
||||
assert profile["balanced"] is True
|
||||
Reference in New Issue
Block a user