fix(769-1): 추천 4 갈래 = 계산 로직 결과 그대로 — 같은 쪽 곡선 합치기 · 측점 맞춤 뺌

- 요구 611: 추천노선 = 평면계획선을 긋는 가이드 선 · 합치기 · 스냅은 예정노선 수정 뒤 평면계획선에만
- _trace = 경로 탐색 → 곡선 넣기(curve_every_kink) → 큰 R(widen) 까지 · merge_same_side · settle 뺌
- 예상노선 추적(merged_follow) · 체인 · /route/plan · replan · follow 는 합치기 · 스냅 그대로
- Pool.snap · Pool.cross · build_pool snap · cross 인자 · 엔진 fit · route_earth · merge_delta 걷음(쓰는 곳 없음)
- ALGO_VERSION 9 → 10
- 낡은 시험 고침: test_419_1 · test_351_2 추천 갈래 측점 위 기대 뺌(추적만 측점 위 · 갈래는 안 맞춤 확인 추가) ·
  test_route_initial 영선 · test_351_2 비중 기울기 상한 기대 뺌(원지반 기울기 참고만 · 비중은 벌점만) ·
  test_224_4 build_pool 엿봄 인자

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-03 16:30:29 +09:00
co-authored by Claude Opus 5.5
parent 2937ba6208
commit f7bd8dbf06
7 changed files with 35 additions and 75 deletions
@@ -12,9 +12,9 @@
· 못 지킨 곳 수(R · 계류 · 측점 — 원지반 기울기는 참고만 · 419 `hard_limits`)가 기준(엄격: 후보
최소 · 풂: 센 벌점 기준 길)보다 많은 후보는 뺀다. 탐색 링크 상한(비탈 곧장 오르는 길 막이)은
남김.
· R = 일반측점이 기본 제약(383-1) — 후보마다 곡선 MC 를 일반측점에 맞춘 선(`snap`)으로 재고
고르고 거름. 못 맞춘 곡선은 「못 지킨 구간」(station) · 고르기는 측점 밖 0 후보를 앞세움.
꺾임만(R 없는 교점)은 안 남김(`..._Radius.curve_every_kink` · 측점 맞춤 · 419).
· 추천 = 평면계획선을 긋는 가이드 선(요구 611 · 769-1) — 계산 로직 결과 그대로(경로 탐색 → 곡선
넣기 → 큰 R) · 같은 쪽 곡선 합치기 · 측점 맞춤은 안 함(평면계획선 길 `..._Follow.merged_follow`
몫). 꺾임만(R 없는 교점)은 안 남김(`..._Radius.curve_every_kink` · 419).
갈래는 **비교값으로** 고른다(`pick` · `GOALS`) — 토공 최소 = 절성토 합 · 기울기 순한 = 최대(같으면
평균) · 영선 노선 = 목표 기울기 편차 · 안전 우선 = 위험 통과 길이(옆경사 35° 이상 · 계류 버퍼 안) ·
@@ -38,7 +38,7 @@ from typing import Any
import numpy as np
from shapely.geometry import LineString
from B05_Profile.B05_Profile_Engine_RouteInitial_Merge import Guard, RouteEarth, merge_same_side
from B05_Profile.B05_Profile_Engine_RouteInitial_Merge import Guard
from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import (
COST_CROSSING,
COST_EARTH_M3,
@@ -52,7 +52,6 @@ from B05_Profile.B05_Profile_Engine_RouteInitial_Search import (
NoRoute,
RouteGraph,
)
from B05_Profile.B05_Profile_Engine_RouteInitial_Station import with_station_misses
from common_util.common_util_route_geometry import densify_route
from common_util.common_util_route_polyline import build_planned_polyline
@@ -87,8 +86,6 @@ TRACE_DENSIFY_M = 2.0
Measure = Callable[..., dict[str, Any]]
Route = tuple[Any, dict[str, Any]]
# (선, 직접 잼) → 곡선 MC 를 일반측점에 맞춘 선(못 맞추면 그 선 그대로)
Snap = Callable[[Any, Any], Any]
@dataclass
@@ -96,7 +93,6 @@ class Pool:
"""같은 입력의 후보 묶음 — 갈래 고르기 · 비중 탐색이 같이 쓴다."""
graph: RouteGraph
snap: Snap
candidates: list[Route] = field(default_factory=list)
bound: float | None = None
limit: int = 0 # 후보로 남길 못 지킨 곳 수 상한
@@ -111,10 +107,8 @@ class Pool:
follow: Route | None = None
# 제약 안에서 갈 수 있는 띠 폭 어림 — 후보끼리 가장 먼 거리(m)
band_width_m: float = 0.0
# 큰 R · 합치기 · 측점 맞춤이 거리 한도 대신 쓰는 직접 잼(406 · `..._Merge.Guard`)
# 큰 R 이 거리 한도 대신 쓰는 직접 잼(406 · `..._Merge.Guard`)
guard: Guard | None = None
# 합치기 멈춤 토공 = 횡단(노선 + 바뀐 측점 차이 · 420 · 없으면 띠 가늠 · `merge_same_side`)
cross: RouteEarth | None = None
# 후보 줄 세움 횡단 토공 기억(잰 값 id · 종단 반영) — 갈래 사이 다시 안 셈(419)
scores: dict[tuple[int, bool], float] = field(default_factory=dict)
@@ -127,15 +121,6 @@ def hard_limits(metrics: dict[str, Any]) -> dict[str, Any]:
return {**metrics, "over_limit": {**over, "total": over["total"] - over.get("grade", 0)}}
def settle(route: Route, snap: Snap, measure: Measure, **kw: Any) -> Route:
"""측점에 맞춘 선 + 잰 값(측점 밖 곡선 = 못 지킨 구간 · 383-1)."""
outline, metrics = route
moved = snap(outline)
if moved is not outline:
outline, metrics = moved, measure(moved, **kw)
return outline, with_station_misses(metrics, outline)
def _trace(
pool: Pool, nodes: list[tuple[float, float]], min_radius_m: float, measure: Measure, **kw: Any
) -> Route:
@@ -144,9 +129,9 @@ def _trace(
# 경로를 예정노선 규칙으로 단순화 · 묶음 · R 맞춤) 차례로, 위반(R · 계류 · 원지반 기울기 뺌)이
# 최소 R 판(노드마다 곡선 · 옛 규칙)보다 많지 않은 첫 판. 모두 화면이 [확인] 때 같은 선으로 다시
# 짜는 꼴. 같은 노드 · 같은 잼이면 다시 재지 않음(쓸기는 이웃 비중이 같은 길을 자주 냄).
# 고른 판의 꺾임만을 곡선 · 교점 지움으로(419) → 같은 쪽 이웃 곡선 합치기(406 · `..._Merge`) →
# 측점에 맞춤(`settle` · 383-1). 큰 R · 합치기 · 측점 맞춤 모두 거리 한도 대신 직접
# 잼(`pool.guard`) · 비중 계산은 예상노선 이격도(그 갈래 잣대 — 예상 몫을 줄이면 이격이 늘기만).
# 고른 판의 꺾임만을 곡선 · 교점 지움으로(419) — 끝. 같은 쪽 곡선 합치기 · 측점 맞춤은 안 함
# (추천 = 가이드 선 · 요구 611). 큰 R 은 거리 한도 대신 직접 잼(`pool.guard`) · 비중 계산은
# 예상노선 이격도(그 갈래 잣대 — 예상 몫을 줄이면 이격이 늘기만).
traced = pool.traced
guard = pool.guard
if guard is not None and kw.get("weighted"):
@@ -176,10 +161,7 @@ def _trace(
line = curve_every_kink(chosen[0], min_radius_m)
if line is not chosen[0]:
chosen = (line, measure(line, **kw))
chosen = merge_same_side(
chosen, guard, min_radius_m, lambda line: measure(line, **kw), pool.cross
)
traced[key] = settle(chosen, lambda line: pool.snap(line, guard), measure, **kw)
traced[key] = chosen
return traced[key]
@@ -252,13 +234,11 @@ def build_pool(
max_grade: float,
min_radius_m: float,
measure: Measure,
snap: Snap,
cross: RouteEarth | None = None,
) -> Pool:
"""후보 묶음 — 제약 탐색 후보 → 끝 노선 셋으로 비중 크기 맞춤 → 토공 ↔ 기울기 쓸기를 더함.
`measure` 는 원지반 기울기를 뺀 못 지킨 곳 수를 내야 함(`hard_limits`)."""
pool = Pool(graph, snap, follow=follow, guard=Guard(graph.grid, max_grade), cross=cross)
pool = Pool(graph, follow=follow, guard=Guard(graph.grid, max_grade))
pool.candidates, pool.bound, pool.limit = _candidates(pool, max_grade, min_radius_m, measure)
candidates, bound, limit = pool.candidates, pool.bound, pool.limit
ends = [follow[0], pick("earthwork", candidates)[0], pick("gentle", candidates)[0]]