feat(B05): 추천 갈래 다시 — 영선 · 안전 우선 · 계류 횡단 최소 · 공사비 어림 최소(PLAN 23-5a)
- 갈래 short · balance 뺌(요청 422) · gradeline · safe · crossings · cost 더함 — 각 목표로 제약 탐색한 후보를 묶음에 넣고 제 비교값으로 고름 - 탐색 목표 셋 더함: 목표 기울기 편차(영선 · 등경사선) · 위험(옆경사 35° 이상 · 계류 버퍼 안 통과 길이) · 계류 횡단 링크 · 공사비는 상대 단가 그대로 - 비교값 더함: side_slope_avg_pct · side_slope_max_pct · hazard_m · stream_crossings · stream_offset_avg_m · grade_dev_pct · cost_index - 공사비 상대 단가(절성토 1 ㎥ = 1 · 연장 1 m = 2 · 계류 횡단 1 곳 = 400)는 임시 상대값 · 브레인 임의 판정 - 요청 criteria.target_grade_pct(영선 목표 기울기 · 없으면 상한) · 응답 criteria.target_grade_pct - 시험 test_route_initial 31 · 왕복 시험은 화면이 아는 갈래만(새 넷은 23-5b 뒤) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
This commit is contained in:
@@ -3,15 +3,17 @@
|
||||
`B05_Profile_Engine_RouteInitial` 이 부름.
|
||||
|
||||
후보 묶음(`build_pool`) = 같은 입력(지형 · 예상노선 · 계류 · 제약)이면 한 번만 짠다:
|
||||
· 제약 탐색 후보 — 목표 하나씩(토공 · 기울기 · 이격 · 연장) × 탐색 상한 단계(지킬 수 있을
|
||||
· 제약 탐색 후보 — 목표 하나씩(`TARGETS` · 토공 · 기울기 · 이격 · 목표 기울기 편차 · 위험 ·
|
||||
계류 횡단 · 공사비) × 탐색 상한 단계(지킬 수 있을
|
||||
때) 또는 벌점 크기(지형상 불가능할 때 · 막은 기울기 안) — PLAN 23-1 그대로.
|
||||
· 토공 ↔ 기울기 쓸기 — 비중 계산(예상 0)을 토공 100 → 기울기 100 사이 `SWEEP_STEPS` 벌.
|
||||
갈래가 그 사이 어느 비중보다 제 목표에서 좋게(토공 최소 ≤ 쓸기 절성토 · 기울기 순한 ≤
|
||||
쓸기 최대 기울기).
|
||||
· 못 지킨 곳 수가 기준(엄격: 후보 최소 · 풂: 센 벌점 기준 길)보다 많은 후보는 뺀다.
|
||||
|
||||
갈래는 **비교값으로** 고른다(`pick`) — 토공 최소 = 절성토 합 · 기울기 순한 = 최대(같으면 평균) ·
|
||||
짧은 노선 = 연장 · 흙 균형 = |절토 − 성토|(같으면 절성토 합).
|
||||
갈래는 **비교값으로** 고른다(`pick` · `GOALS`) — 토공 최소 = 절성토 합 · 기울기 순한 = 최대(같으면
|
||||
평균) · 영선 노선 = 목표 기울기 편차 · 안전 우선 = 위험 통과 길이(옆경사 35° 이상 · 계류 버퍼 안) ·
|
||||
계류 횡단 최소 = 횡단 수(같으면 계류에서 먼 쪽) · 공사비 어림 최소 = 상대 공사비(PLAN 23-5).
|
||||
|
||||
비중 계산(`weighted`)은 고르기가 아니라 **탐색 비용의 가중합** 한 번(PLAN 23-4a):
|
||||
· 몫 = 예상노선에서 벗어난 거리 · 토공 가늠 · 기울기 — 각 몫을 끝 노선 셋(추적 · 토공 최소 ·
|
||||
@@ -29,15 +31,29 @@ from typing import Any
|
||||
import numpy as np
|
||||
from shapely.geometry import LineString
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Search import LENGTH_COST, NoRoute, RouteGraph
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import (
|
||||
COST_CROSSING,
|
||||
COST_EARTH_M3,
|
||||
COST_LENGTH_M,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Search import (
|
||||
CROSSING_BAND,
|
||||
OBJECTIVES,
|
||||
NoRoute,
|
||||
RouteGraph,
|
||||
)
|
||||
from common_util.common_util_route_polyline import build_planned_polyline
|
||||
|
||||
ZERO = {"expected": 0.0, "earthwork": 0.0, "grade": 0.0}
|
||||
TARGETS = {
|
||||
# 갈래 후보를 찾는 탐색 목표(PLAN 23-1 · 23-5) — 공사비는 상대 단가 그대로(`_solve`)
|
||||
TARGETS: dict[str, dict[str, float]] = {
|
||||
"earthwork": {**ZERO, "earthwork": 100.0},
|
||||
"gentle": {**ZERO, "grade": 100.0},
|
||||
"follow": {**ZERO, "expected": 100.0},
|
||||
"short": dict(ZERO), # 목표 몫 없음 — 거리 비용만(`SHORT_LENGTH_COST`)
|
||||
"gradeline": {"gradeline": 100.0},
|
||||
"safe": {"risk": 100.0},
|
||||
"crossings": {"crossing": 100.0},
|
||||
"cost": {},
|
||||
}
|
||||
# 탐색 상한 = 상한 × 이 값들 차례로 — 곡선 맞춤 뒤 측점 사이 지반이 넘으면 다음 값으로 다시 찾음
|
||||
GRADE_CAP_STEPS = (1.0, 0.93, 0.85)
|
||||
@@ -45,8 +61,6 @@ GRADE_CAP_STEPS = (1.0, 0.93, 0.85)
|
||||
# 넘긴 길)은 센 값, 갈래 후보는 목표와 겨룰 만한 값들(목표는 1 m 당 평균 1 로 정규화됨)
|
||||
SOFT_GRADE_COST = 500.0
|
||||
RELAXED_GRADE_COSTS = (50.0, 5.0)
|
||||
# 짧은 노선 탐색의 1 m 당 거리 비용 — 꺾기 벌점(원호 길이)과 같은 단위라 「연장 최소」가 됨
|
||||
SHORT_LENGTH_COST = 1.0
|
||||
# 토공 100 → 기울기 100 쓸기 벌 수(양 끝 포함 · 10 % 걸음)
|
||||
SWEEP_STEPS = 11
|
||||
# 끝 노선 사이 범위가 링크 평균의 이 몫보다 작으면(끝 노선끼리 같음) 링크 평균으로 크기를 맞춤
|
||||
@@ -99,8 +113,21 @@ def _trace(
|
||||
return traced[key]
|
||||
|
||||
|
||||
def _length_cost(weights: dict[str, float]) -> float:
|
||||
return LENGTH_COST if any(weights.values()) else SHORT_LENGTH_COST
|
||||
def _solve(graph: RouteGraph, target: str, **constraint: Any) -> list[tuple[float, float]]:
|
||||
"""갈래 후보 탐색 한 번. 공사비 = 토공 가늠 · 계류 횡단 · 연장을 상대 단가 그대로 더한 비용
|
||||
(링크 평균이 1 쯤 되게 한 값으로 나눔 — 제약 벌점과 겨루는 크기를 다른 목표와 맞춤)."""
|
||||
if target != "cost":
|
||||
return graph.solve(TARGETS[target], **constraint)
|
||||
crossing = COST_CROSSING / (2.0 * CROSSING_BAND * graph.grid.cell) # 1 m 당(횡단 한 곳 몫)
|
||||
per_m = COST_EARTH_M3 * float(np.average(graph.terms[1], weights=graph.length)) + COST_LENGTH_M
|
||||
weights = {"earthwork": COST_EARTH_M3 / per_m, "crossing": crossing / per_m}
|
||||
total = sum(weights.values())
|
||||
return graph.solve(
|
||||
weights,
|
||||
scale=np.full(len(OBJECTIVES), 1.0 / total),
|
||||
length_cost=COST_LENGTH_M / per_m,
|
||||
**constraint,
|
||||
)
|
||||
|
||||
|
||||
def _candidates(
|
||||
@@ -116,15 +143,10 @@ def _candidates(
|
||||
없으면 막은 기울기 안에서 벌점 크기 · 목표를 바꿔 찾은 노선들.
|
||||
"""
|
||||
found: list[Route] = []
|
||||
for weights in TARGETS.values():
|
||||
for target in TARGETS:
|
||||
for step in GRADE_CAP_STEPS:
|
||||
try:
|
||||
nodes = graph.solve(
|
||||
weights,
|
||||
cap=max_grade * step,
|
||||
strict_stream=True,
|
||||
length_cost=_length_cost(weights),
|
||||
)
|
||||
nodes = _solve(graph, target, cap=max_grade * step, strict_stream=True)
|
||||
except NoRoute:
|
||||
break
|
||||
found.append(_trace(nodes, min_radius_m, measure, traced, bound=None))
|
||||
@@ -153,15 +175,9 @@ def _candidates(
|
||||
bound=bound,
|
||||
)
|
||||
found = [seed]
|
||||
for weights in TARGETS.values():
|
||||
for target in TARGETS:
|
||||
for cost in RELAXED_GRADE_COSTS:
|
||||
nodes = graph.solve(
|
||||
weights,
|
||||
cap=cap,
|
||||
strict_stream=False,
|
||||
grade_cost=cost,
|
||||
length_cost=_length_cost(weights),
|
||||
)
|
||||
nodes = _solve(graph, target, cap=cap, strict_stream=False, grade_cost=cost)
|
||||
found.append(_trace(nodes, min_radius_m, measure, traced, bound=bound))
|
||||
most = seed[1]["over_limit"]["total"]
|
||||
return [item for item in found if item[1]["over_limit"]["total"] <= most], bound, most
|
||||
@@ -224,7 +240,7 @@ def _scale(
|
||||
values = np.array([value for value, _ in measured])
|
||||
reference = max(float(np.hypot(*np.diff(np.asarray(lines[0]), axis=0).T).sum()), 1.0)
|
||||
spread = (values.max(axis=0) - values.min(axis=0)) / reference
|
||||
per_link = np.vstack([graph.terms, graph.over / graph.limit, graph.along])
|
||||
per_link = np.vstack([graph.terms[:3], graph.over / graph.limit, graph.along])
|
||||
mean = (per_link * graph.length).sum(axis=1) / max(float(graph.length.sum()), 1e-9)
|
||||
return np.where(spread > MIN_RANGE_SHARE * mean, spread, mean), measured[0][1]
|
||||
|
||||
@@ -241,7 +257,7 @@ def weighted(pool: Pool, weights: dict[str, float], min_radius_m: float, measure
|
||||
strict_stream=False,
|
||||
grade_cost=soft / max(pool.scale[3], 1e-12),
|
||||
stream_cost=soft / max(pool.scale[4], 1e-12),
|
||||
scale=pool.scale[:3],
|
||||
scale=np.concatenate([pool.scale[:3], np.ones(len(OBJECTIVES) - 3)]),
|
||||
)
|
||||
pool.searched[key] = _trace(
|
||||
nodes, min_radius_m, measure, pool.traced, bound=pool.bound, weighted=True
|
||||
@@ -256,8 +272,10 @@ def _earth(metrics: dict[str, Any]) -> float:
|
||||
GOALS: dict[str, Callable[[dict[str, Any]], tuple[float, ...]]] = {
|
||||
"earthwork": lambda m: (_earth(m),),
|
||||
"gentle": lambda m: (m["max_grade_pct"], m["avg_grade_pct"]),
|
||||
"short": lambda m: (m["length_m"], _earth(m)),
|
||||
"balance": lambda m: (abs(m["cut_m3"] - m["fill_m3"]), _earth(m)),
|
||||
"gradeline": lambda m: (m["grade_dev_pct"], m["max_grade_pct"]),
|
||||
"safe": lambda m: (m["hazard_m"], m["side_slope_avg_pct"]),
|
||||
"crossings": lambda m: (m["stream_crossings"], -(m["stream_offset_avg_m"] or 0.0)),
|
||||
"cost": lambda m: (m["cost_index"],),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user