feat(B05 노선 추천 엔진): 합치기 판정 = 바뀐 측점 횡단 차이 · 후보 줄 세움 = 노선 횡단 토공 · 균형 종단 위 5 개 (419-1)
- 420 cross_earthwork · cross_earthwork_delta 이음 — 합치기 멈춤 = 계획고 지반고 노선 횡단 +10 % - 토공 · 공사비 줄 세움 = 상한 누른 계획선 횡단(종단 켬) · 지반고 횡단(종단 끔) - 위 후보 수 3 → 5(검증_sub4 균형 종단 토공 최소 = 줄 세움 5 위) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -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 EarthDelta, Guard, merge_same_side
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Merge import Guard, RouteEarth, merge_same_side
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import (
|
||||
COST_CROSSING,
|
||||
COST_EARTH_M3,
|
||||
@@ -67,8 +67,9 @@ TARGETS: dict[str, dict[str, float]] = {
|
||||
"crossings": {"crossing": 100.0},
|
||||
"cost": {},
|
||||
}
|
||||
# 종단 반영 켬 토공 · 공사비 고르기 — 균형 종단(빠른 판)을 붙일 위 후보 수(419)
|
||||
SHORTLIST = 3
|
||||
# 종단 반영 켬 토공 · 공사비 고르기 — 균형 종단(빠른 판)을 붙일 위 후보 수(419 · 검증_sub4 = 상한 누른
|
||||
# 계획선 횡단 5 위가 균형 종단 토공 최소 → 5)
|
||||
SHORTLIST = 5
|
||||
# 모든 제약을 지키는 길이 없을 때만 쓰는 벌점(1 m 당 × 넘은 기울기 / 상한) — 기준 길(가장 적게
|
||||
# 넘긴 길)은 센 값, 갈래 후보는 목표와 겨룰 만한 값들(목표는 1 m 당 평균 1 로 정규화됨)
|
||||
SOFT_GRADE_COST = 500.0
|
||||
@@ -112,8 +113,10 @@ class Pool:
|
||||
band_width_m: float = 0.0
|
||||
# 큰 R · 합치기 · 측점 맞춤이 거리 한도 대신 쓰는 직접 잼(406 · `..._Merge.Guard`)
|
||||
guard: Guard | None = None
|
||||
# 합치기 멈춤 토공 = 바뀐 측점 횡단 차이(420 · 없으면 노선 잣대 토공 · `merge_same_side`)
|
||||
earth_delta: EarthDelta | None = None
|
||||
# 합치기 멈춤 토공 = 횡단(노선 + 바뀐 측점 차이 · 420 · 없으면 띠 가늠 · `merge_same_side`)
|
||||
cross: RouteEarth | None = None
|
||||
# 후보 줄 세움 횡단 토공 기억(잰 값 id · 종단 반영) — 갈래 사이 다시 안 셈(419)
|
||||
scores: dict[tuple[int, bool], float] = field(default_factory=dict)
|
||||
|
||||
|
||||
def hard_limits(metrics: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -174,7 +177,7 @@ def _trace(
|
||||
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
|
||||
chosen, guard, min_radius_m, lambda line: measure(line, **kw), pool.cross
|
||||
)
|
||||
traced[key] = settle(chosen, lambda line: pool.snap(line, guard), measure, **kw)
|
||||
return traced[key]
|
||||
@@ -250,14 +253,12 @@ def build_pool(
|
||||
min_radius_m: float,
|
||||
measure: Measure,
|
||||
snap: Snap,
|
||||
earth_delta: EarthDelta | None = None,
|
||||
cross: RouteEarth | None = None,
|
||||
) -> Pool:
|
||||
"""후보 묶음 — 제약 탐색 후보 → 끝 노선 셋으로 비중 크기 맞춤 → 토공 ↔ 기울기 쓸기를 더함.
|
||||
|
||||
`measure` 는 원지반 기울기를 뺀 못 지킨 곳 수를 내야 함(`hard_limits`)."""
|
||||
pool = Pool(
|
||||
graph, snap, follow=follow, guard=Guard(graph.grid, max_grade), earth_delta=earth_delta
|
||||
)
|
||||
pool = Pool(graph, snap, follow=follow, guard=Guard(graph.grid, max_grade), cross=cross)
|
||||
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]]
|
||||
@@ -284,15 +285,16 @@ def similar(
|
||||
outline: Any,
|
||||
with_profile: bool = True,
|
||||
profile: Callable[[Route], None] | None = None,
|
||||
earth: CrossScore | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""이 노선과 `SIMILAR_M` 안인 다른 갈래 — 갈래 이름 · 가장 먼 거리(m).
|
||||
|
||||
화면이 「이 지형은 제약 안에서 갈 길이 한 띠뿐 · ○○ 과 거의 같은 노선」을 알리는 데 쓴다.
|
||||
다른 갈래도 그 갈래와 같은 고르기(`choose` · `profile` = 줄 세운 위 후보에 균형 종단)."""
|
||||
다른 갈래도 그 갈래와 같은 고르기(`choose` · `profile` · `earth`)."""
|
||||
here = LineString(outline.vertices)
|
||||
others = {
|
||||
"follow": pool.follow,
|
||||
**{name: choose(name, pool.candidates, with_profile, profile) for name in GOALS},
|
||||
**{name: choose(name, pool.candidates, with_profile, profile, earth) for name in GOALS},
|
||||
}
|
||||
found = []
|
||||
for name, (other, _) in others.items():
|
||||
@@ -359,27 +361,38 @@ GOALS: dict[str, Callable[[dict[str, Any]], tuple[float, ...]]] = {
|
||||
}
|
||||
|
||||
|
||||
# (후보, 종단 반영) → 노선 횡단 토공 ㎥ — 켬 = 상한 누른 계획선 · 끔 = 계획고 지반고(420)
|
||||
CrossScore = Callable[[Route, bool], float]
|
||||
|
||||
|
||||
def choose(
|
||||
mode: str,
|
||||
candidates: list[Route],
|
||||
with_profile: bool,
|
||||
profile: Callable[[Route], None] | None,
|
||||
earth: CrossScore | 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)
|
||||
"""갈래 고르기 — 토공 · 공사비는 노선 횡단 토공(`earth` · 없으면 띠 가늠)으로 줄 세움(419):
|
||||
종단 반영 켬 = 상한 누른 계획선 횡단 위 `SHORTLIST` 개만 `profile`(균형 종단 빠른 판)을 붙여
|
||||
그 사이에서 고름(후보마다 종단을 세우지 않음) · 끔 = 계획고 지반고 횡단으로 바로 고름.
|
||||
나머지 갈래는 `pick` 그대로."""
|
||||
if mode not in ("earthwork", "cost"):
|
||||
return pick(mode, candidates, with_profile)
|
||||
unique = list({id(item[1]): item for item in candidates}.values()) # 같은 노선 겹침 뺌
|
||||
|
||||
def score(item: Route, profiled: bool) -> tuple:
|
||||
plain = plain_metrics(item[1])
|
||||
value = _earth(plain) if earth is None else earth(item, profiled)
|
||||
if mode == "cost": # 공사비 어림의 토공 몫을 횡단 값으로 갈음
|
||||
value += plain["cost_index"] - _earth(plain)
|
||||
return (item[1]["over_limit"].get("station", 0) > 0, value)
|
||||
|
||||
if not (with_profile and profile is not None):
|
||||
return min(unique, key=lambda item: score(item, False))
|
||||
shortlist = sorted(unique, key=lambda item: score(item, True))[:SHORTLIST]
|
||||
for route in shortlist:
|
||||
profile(route)
|
||||
return pick(mode, shortlist, with_profile)
|
||||
|
||||
|
||||
def pick(mode: str, candidates: list[Route], with_profile: bool = True) -> Route:
|
||||
|
||||
Reference in New Issue
Block a user