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:
@@ -4,18 +4,23 @@
|
||||
|
||||
후보 묶음(`build_pool`) = 같은 입력(지형 · 예상노선 · 계류 · 제약)이면 한 번만 짠다:
|
||||
· 제약 탐색 후보 — 목표 하나씩(`TARGETS` · 토공 · 기울기 · 이격 · 목표 기울기 편차 · 위험 ·
|
||||
계류 횡단 · 공사비) × 탐색 상한 단계(지킬 수 있을
|
||||
때) 또는 벌점 크기(지형상 불가능할 때 · 막은 기울기 안) — PLAN 23-1 그대로.
|
||||
계류 횡단 · 공사비) × 탐색 링크 상한(지킬 수 있을 때 · 한 번) 또는 벌점 크기(지형상 불가능할
|
||||
때 · 막은 기울기 안) — PLAN 23-1.
|
||||
· 토공 ↔ 기울기 쓸기 — 비중 계산(예상 0)을 토공 100 → 기울기 100 사이 `SWEEP_STEPS` 벌.
|
||||
갈래가 그 사이 어느 비중보다 제 목표에서 좋게(토공 최소 ≤ 쓸기 절성토 · 기울기 순한 ≤
|
||||
쓸기 최대 기울기).
|
||||
· 못 지킨 곳 수가 기준(엄격: 후보 최소 · 풂: 센 벌점 기준 길)보다 많은 후보는 뺀다.
|
||||
· 못 지킨 곳 수(R · 계류 · 측점 — 원지반 기울기는 참고만 · 419 `hard_limits`)가 기준(엄격: 후보
|
||||
최소 · 풂: 센 벌점 기준 길)보다 많은 후보는 뺀다. 탐색 링크 상한(비탈 곧장 오르는 길 막이)은
|
||||
남김.
|
||||
· R = 일반측점이 기본 제약(383-1) — 후보마다 곡선 MC 를 일반측점에 맞춘 선(`snap`)으로 재고
|
||||
고르고 거름. 못 맞춘 곡선은 「못 지킨 구간」(station) · 고르기는 측점 밖 0 후보를 앞세움.
|
||||
꺾임만(R 없는 교점)은 안 남김(`..._Radius.curve_every_kink` · 측점 맞춤 · 419).
|
||||
|
||||
갈래는 **비교값으로** 고른다(`pick` · `GOALS`) — 토공 최소 = 절성토 합 · 기울기 순한 = 최대(같으면
|
||||
평균) · 영선 노선 = 목표 기울기 편차 · 안전 우선 = 위험 통과 길이(옆경사 35° 이상 · 계류 버퍼 안) ·
|
||||
계류 횡단 최소 = 횡단 수(같으면 계류에서 먼 쪽) · 공사비 어림 최소 = 상대 공사비(PLAN 23-5).
|
||||
종단 반영 켬 토공 · 공사비(`choose`) = 상한 누른 계획선 토공으로 줄 세운 위 `SHORTLIST` 개만 균형
|
||||
종단(빠른 판)을 붙여 고름(419).
|
||||
|
||||
비중 계산(`weighted`)은 고르기가 아니라 **탐색 비용의 가중합** 한 번(PLAN 23-4a):
|
||||
· 몫 = 예상노선에서 벗어난 거리 · 토공 가늠 · 기울기 — 각 몫을 끝 노선 셋(추적 · 토공 최소 ·
|
||||
@@ -33,14 +38,14 @@ from typing import Any
|
||||
import numpy as np
|
||||
from shapely.geometry import LineString
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Merge import Guard, merge_same_side
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Merge import EarthDelta, Guard, merge_same_side
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import (
|
||||
COST_CROSSING,
|
||||
COST_EARTH_M3,
|
||||
COST_LENGTH_M,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import plain_metrics, profile_earth
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Radius import widen
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Radius import curve_every_kink, widen
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Search import (
|
||||
CROSSING_BAND,
|
||||
OBJECTIVES,
|
||||
@@ -62,8 +67,8 @@ TARGETS: dict[str, dict[str, float]] = {
|
||||
"crossings": {"crossing": 100.0},
|
||||
"cost": {},
|
||||
}
|
||||
# 탐색 상한 = 상한 × 이 값들 차례로 — 곡선 맞춤 뒤 측점 사이 지반이 넘으면 다음 값으로 다시 찾음
|
||||
GRADE_CAP_STEPS = (1.0, 0.93, 0.85)
|
||||
# 종단 반영 켬 토공 · 공사비 고르기 — 균형 종단(빠른 판)을 붙일 위 후보 수(419)
|
||||
SHORTLIST = 3
|
||||
# 모든 제약을 지키는 길이 없을 때만 쓰는 벌점(1 m 당 × 넘은 기울기 / 상한) — 기준 길(가장 적게
|
||||
# 넘긴 길)은 센 값, 갈래 후보는 목표와 겨룰 만한 값들(목표는 1 m 당 평균 1 로 정규화됨)
|
||||
SOFT_GRADE_COST = 500.0
|
||||
@@ -102,20 +107,28 @@ class Pool:
|
||||
follow_cap: float = np.inf
|
||||
searched: dict[tuple[float, ...], Route] = field(default_factory=dict)
|
||||
traced: dict[tuple, Route] = field(default_factory=dict) # 노드 → 잰 노선(`_trace`)
|
||||
raw: dict[int, Route] = field(default_factory=dict) # 맞춘 선 id → 맞추기 전(「측점 맞춤」 끔)
|
||||
follow: Route | None = None
|
||||
# 제약 안에서 갈 수 있는 띠 폭 어림 — 후보끼리 가장 먼 거리(m)
|
||||
band_width_m: float = 0.0
|
||||
# 큰 R · 합치기 · 측점 맞춤이 거리 한도 대신 쓰는 직접 잼(406 · `..._Merge.Guard`)
|
||||
guard: Guard | None = None
|
||||
# 합치기 멈춤 토공 = 바뀐 측점 횡단 차이(420 · 없으면 노선 잣대 토공 · `merge_same_side`)
|
||||
earth_delta: EarthDelta | None = None
|
||||
|
||||
|
||||
def settle(route: Route, snap: Snap, measure: Measure, raw: dict[int, Route], **kw: Any) -> Route:
|
||||
"""측점에 맞춘 선 + 잰 값(측점 밖 곡선 = 못 지킨 구간 · 383-1) · 맞추기 전 선은 `raw` 에."""
|
||||
def hard_limits(metrics: dict[str, Any]) -> dict[str, Any]:
|
||||
"""원지반 기울기 = 참고만(419) — 못 지킨 곳 수(`over_limit.total`)에서 뺀 잰 값.
|
||||
|
||||
기울기 넘은 곳은 목록(`violations` · kind grade)과 `over_limit.grade` 에 참고로 남는다."""
|
||||
over = metrics["over_limit"]
|
||||
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:
|
||||
raw[id(moved)] = (outline, with_station_misses(metrics, outline))
|
||||
outline, metrics = moved, measure(moved, **kw)
|
||||
return outline, with_station_misses(metrics, outline)
|
||||
|
||||
@@ -125,12 +138,12 @@ def _trace(
|
||||
) -> Route:
|
||||
# 큰 R(PLAN 328) — 2단 판(허용 이탈 안 가장 큰 R · `..._Radius.widen`) → 2단을 최소 R 판
|
||||
# 노드에 바로 건 판(단순화가 기울기 위반을 늘리는 급한 지형 · 검증_sub8 실측) → 1단 판(탐색
|
||||
# 경로를 예정노선 규칙으로 단순화 · 묶음 · R 맞춤) 차례로, 위반이 최소 R 판(노드마다 곡선 ·
|
||||
# 옛 규칙)보다 많지 않은 첫 판. 모두 화면이 [확인] 때 같은 선으로 다시 짜는 꼴.
|
||||
# 같은 노드 · 같은 잼이면 다시 재지 않음(쓸기는 이웃 비중이 같은 길을 자주 냄 · 묶음마다 따로).
|
||||
# 고른 판의 같은 쪽 이웃 곡선을 합치고(406 · `..._Merge`) 측점에 맞춤(`settle` · 383-1).
|
||||
# 큰 R · 합치기 · 측점 맞춤 모두 거리 한도 대신 직접 잼(`pool.guard`) · 비중 계산은 예상노선
|
||||
# 이격도(그 갈래 잣대 — 예상 몫을 줄이면 이격이 늘기만).
|
||||
# 경로를 예정노선 규칙으로 단순화 · 묶음 · R 맞춤) 차례로, 위반(R · 계류 · 원지반 기울기 뺌)이
|
||||
# 최소 R 판(노드마다 곡선 · 옛 규칙)보다 많지 않은 첫 판. 모두 화면이 [확인] 때 같은 선으로 다시
|
||||
# 짜는 꼴. 같은 노드 · 같은 잼이면 다시 재지 않음(쓸기는 이웃 비중이 같은 길을 자주 냄).
|
||||
# 고른 판의 꺾임만을 곡선 · 교점 지움으로(419) → 같은 쪽 이웃 곡선 합치기(406 · `..._Merge`) →
|
||||
# 측점에 맞춤(`settle` · 383-1). 큰 R · 합치기 · 측점 맞춤 모두 거리 한도 대신 직접
|
||||
# 잼(`pool.guard`) · 비중 계산은 예상노선 이격도(그 갈래 잣대 — 예상 몫을 줄이면 이격이 늘기만).
|
||||
traced = pool.traced
|
||||
guard = pool.guard
|
||||
if guard is not None and kw.get("weighted"):
|
||||
@@ -157,8 +170,13 @@ def _trace(
|
||||
least,
|
||||
)
|
||||
if guard is not None:
|
||||
chosen = merge_same_side(chosen, guard, min_radius_m, lambda line: measure(line, **kw))
|
||||
traced[key] = settle(chosen, lambda line: pool.snap(line, guard), measure, pool.raw, **kw)
|
||||
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.earth_delta
|
||||
)
|
||||
traced[key] = settle(chosen, lambda line: pool.snap(line, guard), measure, **kw)
|
||||
return traced[key]
|
||||
|
||||
|
||||
@@ -184,22 +202,18 @@ def _candidates(
|
||||
) -> tuple[list[Route], float | None, int]:
|
||||
"""제약 탐색 후보들 · 지형상 불가능 때 막은 기울기(아니면 None) · 후보로 남길 못 지킨 곳 상한.
|
||||
|
||||
제약을 지키는 길이 있으면 목표마다 탐색 상한을 `GRADE_CAP_STEPS` 차례로 낮춰 찾은 노선들 ·
|
||||
없으면 막은 기울기 안에서 벌점 크기 · 목표를 바꿔 찾은 노선들.
|
||||
제약을 지키는 길이 있으면 목표마다 탐색 링크 상한으로 찾은 노선들(곡선 맞춤 뒤 원지반
|
||||
기울기는 참고만 — 상한 낮춰 다시 찾지 않음 · 419) · 없으면 막은 기울기 안에서 벌점 크기 ·
|
||||
목표를 바꿔 찾은 노선들.
|
||||
"""
|
||||
graph = pool.graph
|
||||
found: list[Route] = []
|
||||
for target in TARGETS:
|
||||
for step in GRADE_CAP_STEPS:
|
||||
try:
|
||||
nodes = _solve(graph, target, cap=max_grade * step, strict_stream=True)
|
||||
except NoRoute:
|
||||
break
|
||||
found.append(_trace(pool, nodes, min_radius_m, measure, bound=None))
|
||||
if found[-1][1]["over_limit"]["total"] == 0:
|
||||
break
|
||||
if not found: # 제약을 지키는 길 없음 — 목표와 무관하니 더 볼 것 없음
|
||||
break
|
||||
try:
|
||||
nodes = _solve(graph, target, cap=max_grade, strict_stream=True)
|
||||
except NoRoute:
|
||||
break # 제약을 지키는 길 없음 — 목표와 무관하니 더 볼 것 없음
|
||||
found.append(_trace(pool, nodes, min_radius_m, measure, bound=None))
|
||||
if found:
|
||||
least = min(item[1]["over_limit"]["total"] for item in found)
|
||||
return [item for item in found if item[1]["over_limit"]["total"] == least], None, least
|
||||
@@ -236,9 +250,14 @@ def build_pool(
|
||||
min_radius_m: float,
|
||||
measure: Measure,
|
||||
snap: Snap,
|
||||
earth_delta: EarthDelta | None = None,
|
||||
) -> Pool:
|
||||
"""후보 묶음 — 제약 탐색 후보 → 끝 노선 셋으로 비중 크기 맞춤 → 토공 ↔ 기울기 쓸기를 더함."""
|
||||
pool = Pool(graph, snap, follow=follow, guard=Guard(graph.grid, max_grade))
|
||||
"""후보 묶음 — 제약 탐색 후보 → 끝 노선 셋으로 비중 크기 맞춤 → 토공 ↔ 기울기 쓸기를 더함.
|
||||
|
||||
`measure` 는 원지반 기울기를 뺀 못 지킨 곳 수를 내야 함(`hard_limits`)."""
|
||||
pool = Pool(
|
||||
graph, snap, follow=follow, guard=Guard(graph.grid, max_grade), earth_delta=earth_delta
|
||||
)
|
||||
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]]
|
||||
@@ -259,14 +278,21 @@ def build_pool(
|
||||
return pool
|
||||
|
||||
|
||||
def similar(pool: Pool, mode: str, outline: Any, with_profile: bool = True) -> list[dict[str, Any]]:
|
||||
def similar(
|
||||
pool: Pool,
|
||||
mode: str,
|
||||
outline: Any,
|
||||
with_profile: bool = True,
|
||||
profile: Callable[[Route], None] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""이 노선과 `SIMILAR_M` 안인 다른 갈래 — 갈래 이름 · 가장 먼 거리(m).
|
||||
|
||||
화면이 「이 지형은 제약 안에서 갈 길이 한 띠뿐 · ○○ 과 거의 같은 노선」을 알리는 데 쓴다."""
|
||||
화면이 「이 지형은 제약 안에서 갈 길이 한 띠뿐 · ○○ 과 거의 같은 노선」을 알리는 데 쓴다.
|
||||
다른 갈래도 그 갈래와 같은 고르기(`choose` · `profile` = 줄 세운 위 후보에 균형 종단)."""
|
||||
here = LineString(outline.vertices)
|
||||
others = {
|
||||
"follow": pool.follow,
|
||||
**{name: pick(name, pool.candidates, with_profile) for name in GOALS},
|
||||
**{name: choose(name, pool.candidates, with_profile, profile) for name in GOALS},
|
||||
}
|
||||
found = []
|
||||
for name, (other, _) in others.items():
|
||||
@@ -333,6 +359,29 @@ GOALS: dict[str, Callable[[dict[str, Any]], tuple[float, ...]]] = {
|
||||
}
|
||||
|
||||
|
||||
def choose(
|
||||
mode: str,
|
||||
candidates: list[Route],
|
||||
with_profile: bool,
|
||||
profile: Callable[[Route], None] | None,
|
||||
) -> Route:
|
||||
"""갈래 고르기 — 종단 반영 켬 토공 · 공사비는 상한 누른 계획선 토공(종단 전 비교값)으로 줄 세운
|
||||
위 `SHORTLIST` 개만 `profile`(균형 종단 빠른 판)을 붙여 그 사이에서 고름(419 · 후보마다 종단을
|
||||
세우지 않음). 나머지 갈래 · 종단 끔은 `pick` 그대로."""
|
||||
if with_profile and profile is not None and mode in ("earthwork", "cost"):
|
||||
goal = GOALS[mode]
|
||||
candidates = sorted(
|
||||
{id(item[1]): item for item in candidates}.values(), # 같은 노선 겹침 뺌
|
||||
key=lambda item: (
|
||||
item[1]["over_limit"].get("station", 0) > 0,
|
||||
*goal(plain_metrics(item[1])),
|
||||
),
|
||||
)[:SHORTLIST]
|
||||
for route in candidates:
|
||||
profile(route)
|
||||
return pick(mode, candidates, with_profile)
|
||||
|
||||
|
||||
def pick(mode: str, candidates: list[Route], with_profile: bool = True) -> Route:
|
||||
"""갈래 목표 비교값이 가장 좋은 후보 — 측점 밖 곡선 없는 후보 먼저(383-1). 같으면 앞 후보.
|
||||
`with_profile` 끔 = 원지반 가늠 점수."""
|
||||
|
||||
Reference in New Issue
Block a user