Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -471,9 +471,11 @@ def generate_initial_route(
|
||||
snapped = (outline, metrics)
|
||||
|
||||
def finish(route: tuple[Any, dict[str, Any]]) -> dict[str, Any]:
|
||||
line, values = route
|
||||
# 정밀 종단은 낼 사본에만 — 묶음 캐시 후보는 빠른 판 그대로 둬 다음 고르기가 같은 잣대끼리
|
||||
# 견줌(앞서 보인 후보만 정밀이면 캐시 순서 따라 고르는 선이 바뀜 · 419-2)
|
||||
line, values = route[0], dict(route[1])
|
||||
if with_profile:
|
||||
profiled(route)
|
||||
profiled((line, values))
|
||||
if pipes and "error" not in values["profile"]:
|
||||
values["profile"].update(cover_shortfall(values["profile"], line.vertices, pipes))
|
||||
else:
|
||||
|
||||
@@ -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(
|
||||
@@ -249,6 +251,52 @@ def _reference(ground: np.ndarray, spans: np.ndarray, max_grade: float) -> np.nd
|
||||
return 0.5 * (forward + backward)
|
||||
|
||||
|
||||
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 = 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)):
|
||||
z[i] = min(max(z[i], z[i - 1] - rise[i - 1]), z[i - 1] + rise[i - 1])
|
||||
z[-1] = end
|
||||
for i in range(len(z) - 2, -1, -1):
|
||||
z[i] = min(max(z[i], z[i + 1] - rise[i]), z[i + 1] + rise[i])
|
||||
if abs(z[0] - start) < 1e-9:
|
||||
break
|
||||
return z
|
||||
|
||||
|
||||
def held_pass_through(
|
||||
base_s: np.ndarray, stations: list[float], plan: list[float], policy: Any
|
||||
) -> np.ndarray:
|
||||
"""균형 종단(측점 · 계획고)을 1차 변화점에 옮긴 꼭짓점 표고 — 종단곡선을 넣어도 측점마다
|
||||
계획고를 지나게 꼭짓점을 미는 `pass_through` 와 같은 되풀이에 직선 기울기 상한을 함께 둠(419-3).
|
||||
|
||||
밀기만 하면 꼭짓점 사이 직선이 균형 종단 기울기보다 가팔라짐(검증_sub4 14 % → 14.47 %).
|
||||
상한 = 균형 종단 측점 사이 가장 센 기울기(이미 상한 안 · 화면 겹침 상한도 그대로)."""
|
||||
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())
|
||||
# 계획고 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):
|
||||
curves, _warnings = build_curves(base_s, base_z, policy)
|
||||
error = target - evaluate(base_s, base_z, curves, base_s)
|
||||
error[0] = error[-1] = 0.0
|
||||
if float(np.max(np.abs(error))) < 1e-4:
|
||||
break
|
||||
base_z = _hold(base_s, base_z + error, limit)
|
||||
return base_z
|
||||
|
||||
|
||||
def _dp(
|
||||
levels: np.ndarray, cost: np.ndarray, spans: np.ndarray, max_grade: float, delta_max: float
|
||||
) -> np.ndarray | None:
|
||||
|
||||
@@ -360,16 +360,17 @@ def _set_base_pvi(path: Path, stations: list[float], plan: list[float]) -> int:
|
||||
import numpy as np
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import AlignmentPolicy
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import pass_through
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import held_pass_through
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
|
||||
longitudinal = json.loads(path.read_text(encoding="utf-8"))
|
||||
alignment = longitudinal["profile_alignment"]
|
||||
base = alignment["base_pvi"]
|
||||
base_s = np.array([float(item["chainage_m"]) for item in base])
|
||||
# 1차 종단과 같은 맞춤 — 종단곡선을 넣어도 측점마다 균형 종단 계획고를 지나게.
|
||||
base_z = pass_through(
|
||||
base_s, np.interp(base_s, stations, plan), AlignmentPolicy.from_dict(alignment["policy"])
|
||||
# 1차 종단과 같은 맞춤 — 종단곡선을 넣어도 측점마다 균형 종단 계획고를 지나게 · 꼭짓점 사이
|
||||
# 직선은 균형 종단 상한 안(419-3)
|
||||
base_z = held_pass_through(
|
||||
base_s, stations, plan, AlignmentPolicy.from_dict(alignment["policy"])
|
||||
)
|
||||
for item, value in zip(base, base_z, strict=True):
|
||||
item["elevation_m"] = round(float(value), 4)
|
||||
|
||||
@@ -150,8 +150,8 @@ def _route(name: str, earth: float, station: int = 0):
|
||||
|
||||
|
||||
def test_choose_profiles_only_shortlist() -> None:
|
||||
"""종단 반영 켬 토공 = 노선 횡단 토공(줄 세움) 위 `SHORTLIST` 개만 균형 종단 · 그 사이에서 고름 ·
|
||||
줄 세움 = `earth`(켬 = 상한 누른 계획선 · 끔 = 지반고) · 없으면 띠 가늠."""
|
||||
"""종단 반영 켬 토공 = 노선 횡단 토공(줄 세움) 위 `SHORTLIST` 개만 균형 종단 · 그 사이에서
|
||||
고름 · 줄 세움 = `earth`(켬 = 상한 누른 계획선 · 끔 = 지반고) · 없으면 띠 가늠."""
|
||||
routes = [_route(f"r{i}", 100.0 + 10 * i) for i in range(12)] + [_route("off", 1.0, 1)]
|
||||
last = f"r{SHORTLIST - 1}"
|
||||
profiled = []
|
||||
@@ -212,3 +212,67 @@ def test_hill_branches_rules(hill_runs: dict, mode: str) -> None:
|
||||
assert over["total"] == over["radius"] + over["stream"] + over.get("station", 0)
|
||||
assert "unsnapped" not in result
|
||||
assert result["ends_over_limit"] is False and result["ends_grade_pct"] < 14.0
|
||||
|
||||
|
||||
def _hill_expected():
|
||||
x = np.arange(0.0, 802.0, 2.0)
|
||||
y = np.arange(0.0, 402.0, 2.0)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
hill = Terrain(x, y, 40.0 * np.exp(-((xx - 400.0) ** 2 + (yy - 200.0) ** 2) / 90.0**2), xx > -1)
|
||||
expected = [(float(v), 200.0 + 40.0 * math.sin(v / 60.0)) for v in np.arange(50.0, 752.0, 4.0)]
|
||||
return hill, expected
|
||||
|
||||
|
||||
def test_419_2_pick_independent_of_cache_order() -> None:
|
||||
"""같은 입력 — 앞서 다른 갈래를 보였든(캐시 후보에 정밀 종단) 처음이든 같은 선(419-2)."""
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial import _POOLS
|
||||
|
||||
hill, expected = _hill_expected()
|
||||
|
||||
def run(mode):
|
||||
return generate_initial_route(
|
||||
expected, hill, [], mode=mode, weights=None, max_grade=0.14, min_radius_m=RADIUS
|
||||
)
|
||||
|
||||
_POOLS.clear()
|
||||
fresh = {mode: run(mode)["planned"] for mode in ("earthwork", "cost")}
|
||||
_POOLS.clear()
|
||||
for mode in ("gentle", "gradeline", "weighted", "cost", "earthwork", "gentle"):
|
||||
run(mode)
|
||||
again = {mode: run(mode)["planned"] for mode in ("earthwork", "cost")}
|
||||
# 캐시 후보에 정밀 종단이 안 남음 — 고르기는 늘 빠른 판끼리(정밀은 낼 사본에만)
|
||||
pool = next(iter(_POOLS.values()))
|
||||
routes = [*pool.candidates, *pool.searched.values()]
|
||||
assert not any((m.get("profile") or {}).get("exact") for _, m in routes)
|
||||
_POOLS.clear()
|
||||
assert again == fresh
|
||||
|
||||
|
||||
def test_419_3_applied_profile_holds_grade() -> None:
|
||||
"""[적용] 종단 — 종단곡선을 넣으려 꼭짓점을 밀어도 직선 · 곡선 어디서나 균형 종단 상한 안."""
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import (
|
||||
AlignmentPolicy,
|
||||
build_curves,
|
||||
evaluate,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import pass_through
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import held_pass_through
|
||||
|
||||
policy = AlignmentPolicy.from_config(
|
||||
station_interval_m=20.0, max_grade_pct=14.0, curve_skip_delta_pct=5.0, paved=False
|
||||
)
|
||||
s = np.arange(0.0, 401.0, 20.0)
|
||||
grades = [0.14, 0.14, -0.02, 0.14, 0.14, 0.03, -0.14, -0.14, 0.0, 0.14]
|
||||
grades += [0.10, 0.14, -0.05, -0.14, -0.14, 0.02, 0.14, 0.14, 0.0, -0.14]
|
||||
plan = np.concatenate([[100.0], 100.0 + np.cumsum(np.asarray(grades) * 20.0)])
|
||||
fine = np.linspace(0.0, 400.0, 4001)
|
||||
|
||||
def steepest(z):
|
||||
curves, _ = build_curves(s, z, policy)
|
||||
return float(np.abs(np.diff(evaluate(s, z, curves, fine)) / 0.1).max())
|
||||
|
||||
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:
|
||||
|
||||
@@ -209,10 +209,6 @@ def _check_weight_monotone(run) -> None:
|
||||
assert tops[0] <= min(tops) + 1e-6, tops # 끝 0/0/100 = 기울기 순한
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="엔진 pick 이 정밀 · 빠른 판 종단을 섞어 견줌 → 캐시 순서 의존 · PLAN 419 에서 풂",
|
||||
strict=False,
|
||||
)
|
||||
def test_weight_ends_match_branches(hill: Terrain) -> None:
|
||||
_check_weight_ends(lambda mode, weights=None: _run(hill, mode, weights=weights))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user