fix(B05 노선 추천 엔진): 원지반 기울기 참고만 · 삼각 없음 · 측점 필수 · 합치기 멈춤 노선 토공 +10 % · 토공 고르기 위 3 개만 균형 종단 (419-1)
- 못 지킨 곳 수 = R · 계류 · 측점(원지반 기울기는 목록 · grade 수에 참고) · 후보 거름 · 큰 R 판 · 합치기 · 측점 맞춤의 기울기 막기 걷음 · 탐색 링크 상한 남김 - 꺾임만 → 곡선(155° 이상도) 또는 교점 지움 · 측점 맞춤 곡선 끔 · 늦춤 단계 걷음 - 맞추기 전 선(unsnapped) 걷음 · 시종점 기울기 ends_grade_pct · ends_over_limit - 검증_sub4 첫 갈래 122.6 s → 23.8 s · 곡선 33 → 16~18 · 세 갈래 다른 선 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -28,9 +28,11 @@
|
||||
가파른 지형(옆경사 중앙값 67 %)에서는 제약 안의 길이 폭 10~20 m 한 띠뿐이라 갈래끼리 가까울 수
|
||||
있다 — 응답 `similar`(10 m 안인 다른 갈래) · `band_width_m`(띠 폭 어림)로 알린다(PLAN 23-4).
|
||||
|
||||
공통 제약(follow 밖 모든 갈래 · **넘지 못함**) — 종단기울기 상한 · 최소 곡선반지름 · 계류 이격:
|
||||
· 종단기울기 — 지반 기울기가 상한을 넘는 링크는 탐색에서 뺌. 곡선 맞춤 뒤 측점(20 m) 사이
|
||||
지반이 상한을 넘으면 탐색 상한을 조금씩 낮춰 다시 찾음(`GRADE_CAP_STEPS`).
|
||||
공통 제약(follow 밖 모든 갈래 · **넘지 못함**) — 최소 곡선반지름 · 계류 이격 · 곡선 MC = 일반측점:
|
||||
· 종단기울기 — 지반 기울기가 상한을 넘는 링크는 탐색에서 뺌(비탈 곧장 오르는 길 막이). 곡선
|
||||
맞춤 뒤 측점(20 m) 사이 **원지반 기울기는 참고만**(계획선 높이로 푸는 값 · 요구 253-1 · 419) —
|
||||
못 지킨 곳 수(`over_limit.total`)에 안 듦 · 목록엔 남음. 시종점 높이 차가 상한을 넘으면
|
||||
`ends_over_limit`(종단으로도 못 풂 · 화면 알림).
|
||||
· 최소 곡선반지름 — 한 번에 이웃 방향(약 22.5°)으로만 꺾고, 꺾은 뒤 원호 두 개의 접선 길이만큼
|
||||
곧게 가야 다시 꺾음 → 노드마다 최소 반지름 원호가 들어감.
|
||||
· 계류 이격 — 버퍼 안에서는 계류 쪽 · 반대쪽 ±45° 로만 움직임(건너기 · 빠져나가기).
|
||||
@@ -61,7 +63,8 @@ from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import GRADE_SMOOTH_M,
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Pool import (
|
||||
Pool,
|
||||
build_pool,
|
||||
pick,
|
||||
choose,
|
||||
hard_limits,
|
||||
settle,
|
||||
similar,
|
||||
weighted,
|
||||
@@ -303,21 +306,24 @@ def generate_initial_route(
|
||||
def measure(
|
||||
outline: Any, *, bound: float | None, follow: bool = False, weighted: bool = False
|
||||
) -> dict[str, Any]:
|
||||
return route_metrics(
|
||||
outline.vertices,
|
||||
expected,
|
||||
terrain,
|
||||
streams,
|
||||
outline.curves,
|
||||
max_grade=max_grade,
|
||||
min_radius_m=min_radius_m,
|
||||
stream_offset_m=stream_offset,
|
||||
road_width_m=ROAD_WIDTH_M,
|
||||
bound_pct=None if bound is None else bound * 100.0,
|
||||
grade_smooth_m=grade_smooth,
|
||||
follow=follow,
|
||||
weighted=weighted,
|
||||
target_grade=goal_grade,
|
||||
# 못 지킨 곳 수 = R · 계류 · 측점 — 원지반 기울기는 참고만(목록엔 남음 · 419)
|
||||
return hard_limits(
|
||||
route_metrics(
|
||||
outline.vertices,
|
||||
expected,
|
||||
terrain,
|
||||
streams,
|
||||
outline.curves,
|
||||
max_grade=max_grade,
|
||||
min_radius_m=min_radius_m,
|
||||
stream_offset_m=stream_offset,
|
||||
road_width_m=ROAD_WIDTH_M,
|
||||
bound_pct=None if bound is None else bound * 100.0,
|
||||
grade_smooth_m=grade_smooth,
|
||||
follow=follow,
|
||||
weighted=weighted,
|
||||
target_grade=goal_grade,
|
||||
)
|
||||
)
|
||||
|
||||
def fit(outline: Any, guard: Any = None) -> Any:
|
||||
@@ -326,14 +332,12 @@ def generate_initial_route(
|
||||
moved = snap_curves_to_stations(outline, min_radius_m, guard=guard)
|
||||
return outline if moved is None else moved
|
||||
|
||||
unsnapped: dict[int, Any] = {}
|
||||
|
||||
def follow_line() -> tuple[Any, dict[str, Any]]:
|
||||
# 예상노선 추적 = 전처리 초기값(`planned_route_initial.csv` · 383-1 측점 맞춤)과 같은
|
||||
# 규칙 · 같은 입력 → 같은 선. 제약은 따지지 않고 넘은 곳만 목록에 뜸.
|
||||
outline = build_planned_polyline(expected, min_radius_m=min_radius_m)
|
||||
route = (outline, measure(outline, bound=None, follow=True))
|
||||
return settle(route, fit, measure, unsnapped, bound=None, follow=True)
|
||||
return settle(route, fit, measure, bound=None, follow=True)
|
||||
|
||||
def profiled(route: Any, exact: bool = True) -> None:
|
||||
if route is not None:
|
||||
@@ -347,6 +351,9 @@ def generate_initial_route(
|
||||
exact=exact,
|
||||
)
|
||||
|
||||
def quick(route: Any) -> None:
|
||||
profiled(route, exact=False)
|
||||
|
||||
bound = None
|
||||
pool: Pool | None = None
|
||||
target = _weighted_end(used) if mode == "weighted" else mode
|
||||
@@ -369,19 +376,19 @@ def generate_initial_route(
|
||||
graph = RouteGraph(grid, expected, limit=max_grade, min_radius_m=min_radius_m)
|
||||
while len(_POOLS) >= POOL_CACHE_SIZE:
|
||||
_POOLS.pop(next(iter(_POOLS)))
|
||||
# 합치기 멈춤 토공 — 420 `cross_earthwork_delta`(바뀐 측점 횡단 차이)가 오면 `earth_delta`
|
||||
# 로 이음 · 그 전엔 노선 잣대 토공(띠 가늠 · `..._Merge.merge_same_side`)
|
||||
_POOLS[key] = build_pool(graph, follow_line(), max_grade, min_radius_m, measure, fit)
|
||||
pool = _POOLS[key]
|
||||
bound = pool.bound
|
||||
if target == "weighted":
|
||||
outline, metrics = weighted(pool, used, min_radius_m, measure)
|
||||
else:
|
||||
if with_profile:
|
||||
for route in [*pool.candidates, pool.follow]:
|
||||
profiled(route, exact=False) # 줄 세우기 — 고른 하나만 아래에서 바로 셈
|
||||
outline, metrics = pick(target, pool.candidates, with_profile)
|
||||
# 후보는 이미 측점에 맞춘 선(383-1 · `..._Pool.settle`) — 모달 「측점 맞춤」 끔 = 맞추기 전 선
|
||||
# 종단 반영 켬 토공 · 공사비 = 줄 세운 위 후보만 균형 종단(빠른 판) · 고른 하나만
|
||||
# 아래에서 바로 셈(419)
|
||||
outline, metrics = choose(target, pool.candidates, with_profile, quick)
|
||||
# 후보는 이미 측점에 맞춘 선(383-1 · `..._Pool.settle` · 맞추기 전 선은 안 냄 · 419)
|
||||
snapped = (outline, metrics)
|
||||
raw = unsnapped.get(id(outline)) or (pool.raw.get(id(outline)) if pool else None)
|
||||
|
||||
def finish(route: tuple[Any, dict[str, Any]]) -> dict[str, Any]:
|
||||
line, values = route
|
||||
@@ -401,6 +408,7 @@ def generate_initial_route(
|
||||
}
|
||||
|
||||
shown = finish(snapped)
|
||||
ends = ends_grade(expected, terrain, shown["metrics"]["length_m"])
|
||||
return {
|
||||
"mode": mode,
|
||||
"logic": mode,
|
||||
@@ -409,10 +417,11 @@ def generate_initial_route(
|
||||
# 지형상 불가능할 때만 — 복도 안 어떤 길도 넘어야 하는 기울기(%) · 그 위는 막았음
|
||||
"terrain_bound_pct": None if bound is None else round(bound * 100.0, 2),
|
||||
**shown,
|
||||
# 측점 맞춤 끔 = 고른 선 그대로(맞출 곡선이 없거나 못 맞췄으면 None — 위와 같음)
|
||||
"unsnapped": None if raw is None else finish(raw),
|
||||
# 시종점 높이 차 ÷ 노선 연장(%) — 상한 넘으면 종단으로도 못 풂(노선을 늘려야 함 · 화면 알림)
|
||||
"ends_grade_pct": ends,
|
||||
"ends_over_limit": ends > max_grade * 100.0 + 1e-6,
|
||||
# 10 m 안인 다른 갈래(같은 노선 알림) · 제약 안 띠 폭 어림 — 추적은 묶음이 이미 있을 때만
|
||||
"similar": None if pool is None else similar(pool, target, outline, with_profile),
|
||||
"similar": None if pool is None else similar(pool, target, outline, with_profile, quick),
|
||||
"with_profile": with_profile,
|
||||
"band_width_m": None if pool is None else round(pool.band_width_m, 1),
|
||||
"criteria": {
|
||||
@@ -429,6 +438,18 @@ def generate_initial_route(
|
||||
}
|
||||
|
||||
|
||||
def ends_grade(expected: list[tuple[float, float]], terrain: Terrain, length_m: float) -> float:
|
||||
"""시종점 지반 높이 차 ÷ 노선 연장(%) — 균형 종단이 끝점을 지반고에 두므로 이 값이 상한을 넘으면
|
||||
어떤 계획선도 상한 안에 못 듦(419 · 원지반 기울기를 참고로 돌린 뒤 남는 알림)."""
|
||||
from scipy.interpolate import RegularGridInterpolator
|
||||
|
||||
sample = RegularGridInterpolator(
|
||||
(terrain.y, terrain.x), terrain.z, bounds_error=False, fill_value=None
|
||||
)
|
||||
z = sample([expected[0][::-1], expected[-1][::-1]])
|
||||
return round(abs(float(z[1] - z[0])) / max(length_m, 1e-9) * 100.0, 2)
|
||||
|
||||
|
||||
def _weighted_end(weights: dict[str, float]) -> str:
|
||||
"""비중이 한쪽 100 이면 그 갈래(끝값 = 갈래와 같은 선 · PLAN 23-3e) · 아니면 비중 탐색."""
|
||||
for name, mode in (("expected", "follow"), ("earthwork", "earthwork"), ("grade", "gentle")):
|
||||
|
||||
Reference in New Issue
Block a user