fix(B05 노선 추천 [적용] 종단): 직선 기울기 누를 때 표고 반올림 여유 0.1 mm · 상한과 같은 균형 종단은 상한으로 (419-3)

- 검증_sub4 [적용] 뒤 종단 최대 14.47 % → 14.001 %(정본 0.1 mm 반올림 몫) → 14.0 %(넘음 표시 없음)
- test_74 변화점 계획고 비교를 0.5 mm 허용차로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
2026-10-18 13:26:18 +09:00
co-authored by Claude Opus 5.5
parent bb0046b524
commit 3082007dba
3 changed files with 14 additions and 4 deletions
@@ -63,6 +63,8 @@ LAMBDA_STEPS = 14
EXACT_STEPS = 6 # 고른 계획선의 실제 누가토량으로 λ 를 더 좁히는 횟수
PIPE_SNAP_M = 5.0 # 관 자리가 노선에서 이 안이면 그 노선의 관으로 봄
GRADE_EPS = 1e-6
# [적용] 종단 꼭짓점 표고 반올림(0.1 mm) 두 끝 몫 — 직선 기울기 누를 때 남기는 여유(m · 419-3)
ROUND_SLACK_M = 1e-4
def _sample(
@@ -251,10 +253,11 @@ def _reference(ground: np.ndarray, spans: np.ndarray, max_grade: float) -> np.nd
def _hold(s: np.ndarray, z: np.ndarray, limit: float) -> np.ndarray:
"""변화점 사이 직선 기울기를 `limit` 안으로 — 시종점은 그대로 두고 앞으로 · 뒤로 누름(시종점
사이가 상한 안이면 몇 번 안에 둘 다 맞음)."""
사이가 상한 안이면 몇 번 안에 둘 다 맞음). 마디마다 `ROUND_SLACK_M` 남김 — 종단 정본이 표고를
0.1 mm 로 반올림해도 상한 안(검증_sub4 14.001 %)."""
z = z.copy()
start, end = z[0], z[-1]
rise = limit * np.diff(s)
rise = np.maximum(limit * np.diff(s) - ROUND_SLACK_M, 0.0)
for _ in range(4):
z[0] = start
for i in range(1, len(z)):
@@ -278,7 +281,10 @@ def held_pass_through(
from B05_Profile.B05_Profile_Engine_Grade_Alignment import build_curves, evaluate
stations_a, plan_a = np.asarray(stations, float), np.asarray(plan, float)
limit = float(np.abs(np.diff(plan_a) / np.diff(stations_a)).max()) + GRADE_EPS
limit = float(np.abs(np.diff(plan_a) / np.diff(stations_a)).max())
# 계획고 1 mm 반올림(`plan_m`)으로 상한을 살짝 넘은 몫은 상한으로
legal = policy.max_grade_pct / 100.0
limit = legal if legal <= limit <= legal + 1e-4 else limit + GRADE_EPS
target = np.interp(base_s, stations_a, plan_a)
base_z = _hold(base_s, target, limit)
for _ in range(12):
@@ -274,4 +274,5 @@ def test_419_3_applied_profile_holds_grade() -> None:
assert steepest(pass_through(s, plan, policy)) > 0.15 # 옛 맞춤 = 상한 넘음(재현)
held = held_pass_through(s, list(s), list(plan), policy)
assert steepest(held) <= 0.14 + 1e-5
assert steepest(np.round(held, 4)) <= 0.14 # 정본 0.1 mm 반올림 뒤도 상한 안
assert held[0] == pytest.approx(plan[0]) and held[-1] == pytest.approx(plan[-1])
@@ -6,6 +6,8 @@
import json
from pathlib import Path
import pytest
from B05_Profile.B05_Profile_Router_Replan import RouteReplanRequest
from B05_Profile.B05_Profile_Router_RouteInitial import _set_base_pvi
from B06_Section.B06_Section_Router_Design import CARRY_INPUT_KEYS
@@ -47,7 +49,8 @@ def test_변화점_계획고를_균형_종단으로(tmp_path: Path) -> None:
p["elevation_m"]
for p in json.loads(path.read_text("utf-8"))["profile_alignment"]["base_pvi"]
]
assert got == [10.0, 12.0, 13.0, 14.0] # 구조물 측점(30 m)은 직선 보간
# 구조물 측점(30 m)은 직선 보간 · 상한 누를 때 반올림 여유 0.1 mm 몫(419-3)
assert got == pytest.approx([10.0, 12.0, 13.0, 14.0], abs=5e-4)
def test_확인_요청_기본은_끔() -> None: