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:
@@ -54,11 +54,18 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from shapely.geometry import LineString
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_CrossEarth import (
|
||||
cross_earthwork,
|
||||
cross_earthwork_delta,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Merge import RouteEarth
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import GRADE_SMOOTH_M, route_metrics
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Pool import (
|
||||
Pool,
|
||||
@@ -70,6 +77,8 @@ from B05_Profile.B05_Profile_Engine_RouteInitial_Pool import (
|
||||
weighted,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import (
|
||||
STATION_M,
|
||||
_reference,
|
||||
attach_profile,
|
||||
cover_shortfall,
|
||||
plain_metrics,
|
||||
@@ -300,9 +309,10 @@ def generate_initial_route(
|
||||
후보 묶음(`..._Pool`)에서 제 목표 비교값이 가장 좋은 것. `stream_offset_m`·`grade_smooth_m` 을
|
||||
안 주면(None) 이 머리의 기본값을 쓴다 — 화면에서 프로젝트마다 겹쳐 쓸 수 있다(PLAN 20장 확정).
|
||||
|
||||
후보마다 균형 종단(`..._Profile` · PLAN 63-4)을 붙여 토공 점수를 그것으로 고른다 — 편집 창
|
||||
추천 점수 · 미리 보기에만 쓰고 확정 종단과 무관. `pipes`(x, y, 덮개 여유)는 덮개 부족 수만 센다.
|
||||
`with_profile` 끄면(종단 반영 끔) 종단 없이 63-4 전 점수(원지반 가늠)로 고른다.
|
||||
토공 · 공사비 갈래는 후보를 노선 횡단 토공(`..._CrossEarth` · 상한 누른 계획선)으로 줄 세워 위
|
||||
몇 개에만 균형 종단(`..._Profile` · PLAN 63-4)을 붙여 고른다(419) — 편집 창 추천 점수 · 미리
|
||||
보기에만 쓰고 확정 종단과 무관. `pipes`(x, y, 덮개 여유)는 덮개 부족 수만 센다.
|
||||
`with_profile` 끄면(종단 반영 끔) 종단 없이 계획고 지반고 횡단 토공으로 고른다.
|
||||
"""
|
||||
if mode not in MODES:
|
||||
raise ValueError(f"모르는 갈래: {mode}")
|
||||
@@ -365,6 +375,58 @@ def generate_initial_route(
|
||||
def quick(route: Any) -> None:
|
||||
profiled(route, exact=False)
|
||||
|
||||
def cross(line: Any, at: Any = None, design_z: Any = None) -> dict[str, Any]:
|
||||
return cross_earthwork(
|
||||
line.vertices,
|
||||
at,
|
||||
design_z,
|
||||
terrain,
|
||||
standard,
|
||||
design_curves=[curve.as_dict() for curve in line.curves],
|
||||
)
|
||||
|
||||
def route_earth(line: Any) -> float:
|
||||
# 합치기 멈춤 잣대 = 계획고 지반고 횡단(1차 종단 규칙 · 종단 끔과 같음)
|
||||
try:
|
||||
found = cross(line)
|
||||
except ValueError:
|
||||
return math.inf
|
||||
return found["cut"] + found["fill"]
|
||||
|
||||
def merge_delta(before: Any, after: Any, window: tuple[float, float]) -> float:
|
||||
# 바뀐 자리 측점만 — 뒤 측점은 같은 선을 따라 밀린 것뿐(옛 선 끝 s1 → 새 선 s1 + 연장 차)
|
||||
shift = LineString(after.vertices).length - LineString(before.vertices).length
|
||||
first = math.ceil(window[0] / STATION_M) * STATION_M
|
||||
try:
|
||||
return cross_earthwork_delta(
|
||||
before.vertices,
|
||||
after.vertices,
|
||||
np.arange(first, window[1] + shift + 1e-9, STATION_M),
|
||||
terrain,
|
||||
standard,
|
||||
before_chainages=np.arange(first, window[1] + 1e-9, STATION_M),
|
||||
design_curves_before=[curve.as_dict() for curve in before.curves],
|
||||
design_curves_after=[curve.as_dict() for curve in after.curves],
|
||||
)["earth"]
|
||||
except ValueError:
|
||||
return math.inf
|
||||
|
||||
def cross_score(route: Any, capped: bool) -> float:
|
||||
# 후보 줄 세움(419) — 켬 = 상한 누른 계획선(`_reference`) 횡단 · 끔 = 계획고 지반고 횡단
|
||||
held = pool.scores if pool is not None else {}
|
||||
key = (id(route[1]), capped)
|
||||
if key not in held:
|
||||
try:
|
||||
found = cross(route[0])
|
||||
if capped:
|
||||
at = np.asarray([row["chainage_m"] for row in found["per_station"]])
|
||||
ground = np.asarray([row["ground_m"] for row in found["per_station"]])
|
||||
found = cross(route[0], at, _reference(ground, np.diff(at), max_grade))
|
||||
held[key] = found["cut"] + found["fill"]
|
||||
except ValueError:
|
||||
held[key] = math.inf
|
||||
return held[key]
|
||||
|
||||
bound = None
|
||||
pool: Pool | None = None
|
||||
target = _weighted_end(used) if mode == "weighted" else mode
|
||||
@@ -387,17 +449,24 @@ 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)
|
||||
# 합치기 멈춤 토공 = 노선 횡단 + 바뀐 측점 횡단 차이(420 `..._CrossEarth` · 419)
|
||||
_POOLS[key] = build_pool(
|
||||
graph,
|
||||
follow_line(),
|
||||
max_grade,
|
||||
min_radius_m,
|
||||
measure,
|
||||
fit,
|
||||
cross=RouteEarth(route_earth, merge_delta),
|
||||
)
|
||||
pool = _POOLS[key]
|
||||
bound = pool.bound
|
||||
if target == "weighted":
|
||||
outline, metrics = weighted(pool, used, min_radius_m, measure)
|
||||
else:
|
||||
# 종단 반영 켬 토공 · 공사비 = 줄 세운 위 후보만 균형 종단(빠른 판) · 고른 하나만
|
||||
# 아래에서 바로 셈(419)
|
||||
outline, metrics = choose(target, pool.candidates, with_profile, quick)
|
||||
# 토공 · 공사비 = 횡단 토공으로 줄 세움 · 종단 반영 켬이면 위 후보만 균형 종단(빠른
|
||||
# 판) · 고른 하나만 아래에서 바로 셈(419)
|
||||
outline, metrics = choose(target, pool.candidates, with_profile, quick, cross_score)
|
||||
# 후보는 이미 측점에 맞춘 선(383-1 · `..._Pool.settle` · 맞추기 전 선은 안 냄 · 419)
|
||||
snapped = (outline, metrics)
|
||||
|
||||
@@ -432,7 +501,9 @@ def generate_initial_route(
|
||||
"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, quick),
|
||||
"similar": None
|
||||
if pool is None
|
||||
else similar(pool, target, outline, with_profile, quick, cross_score),
|
||||
"with_profile": with_profile,
|
||||
"band_width_m": None if pool is None else round(pool.band_width_m, 1),
|
||||
"criteria": {
|
||||
|
||||
@@ -24,6 +24,7 @@ from __future__ import annotations
|
||||
import copy
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
@@ -233,8 +234,13 @@ def _earth(metrics: dict[str, Any]) -> float:
|
||||
return metrics["cut_m3"] + metrics["fill_m3"]
|
||||
|
||||
|
||||
# (합치기 전 선, 합친 선, 바뀐 자리 누가거리(앞 선 기준 s0, s1)) → 노선 토공 차(㎥ · 횡단)
|
||||
EarthDelta = Callable[[PlannedPolyline, PlannedPolyline, tuple[float, float]], float]
|
||||
@dataclass
|
||||
class RouteEarth:
|
||||
"""합치기 멈춤 잣대 — 노선 토공(횡단 ㎥ · `..._CrossEarth`)과 바뀐 측점 횡단 차이."""
|
||||
|
||||
total: Callable[[PlannedPolyline], float]
|
||||
# (합치기 전 선, 합친 선, 바뀐 자리 누가거리(앞 선 기준 s0, s1)) → 노선 토공 차(㎥)
|
||||
delta: Callable[[PlannedPolyline, PlannedPolyline, tuple[float, float]], float]
|
||||
|
||||
|
||||
def merge_same_side(
|
||||
@@ -242,21 +248,23 @@ def merge_same_side(
|
||||
guard: Guard,
|
||||
min_radius_m: float,
|
||||
measure: Callable[[PlannedPolyline], dict[str, Any]],
|
||||
cross_earthwork_delta: EarthDelta | None = None,
|
||||
route_earth: RouteEarth | None = None,
|
||||
) -> Route:
|
||||
"""같은 쪽 이웃 꺾임 합치기를 되풀이한 선과 잰 값 · 합칠 것 없으면 받은 그대로.
|
||||
|
||||
멈춤 = 못 지킨 곳 수(R · 계류 · 원지반 기울기 뺌) 늘어남 · 노선 토공이 합치기 전 선의
|
||||
1 + `TOTAL_EARTH_TOL` 넘음. `cross_earthwork_delta` 주면 토공 = 바뀐 측점 횡단 차이 누적 ·
|
||||
없으면 노선 잣대 토공(`measure` 절 + 성)."""
|
||||
1 + `TOTAL_EARTH_TOL` 넘음. `route_earth` 주면 토공 = 횡단(합치기 전 선 전체 + 바뀐 측점 차이
|
||||
누적) · 없으면 노선 잣대 토공(`measure` 절 + 성 · 띠 가늠)."""
|
||||
outline, base = route
|
||||
if len(outline.curves) < 2:
|
||||
return route
|
||||
points, flags, radii = _flat(outline)
|
||||
points = [(round(x, 4), round(y, 4)) for x, y in points]
|
||||
current = route
|
||||
limit = _earth(base) * (1 + TOTAL_EARTH_TOL)
|
||||
earth = _earth(base) # 지금 선 토공 — 차이를 누적
|
||||
earth = (
|
||||
_earth(base) if route_earth is None else route_earth.total(outline)
|
||||
) # 지금 선 토공 — 차이를 누적
|
||||
limit = earth * (1 + TOTAL_EARTH_TOL)
|
||||
blocked: set[tuple[Xy, ...]] = set()
|
||||
while True:
|
||||
reference = _Reference(current[0].vertices)
|
||||
@@ -291,11 +299,11 @@ def merge_same_side(
|
||||
radii=trial_radii,
|
||||
)
|
||||
metrics = measure(line)
|
||||
if cross_earthwork_delta is None:
|
||||
if route_earth is None:
|
||||
total = _earth(metrics)
|
||||
else:
|
||||
window = sorted(reference.line.project(Point(xy)) for xy in (local[0], local[-1]))
|
||||
total = earth + cross_earthwork_delta(current[0], line, (window[0], window[1]))
|
||||
total = earth + route_earth.delta(current[0], line, (window[0], window[1]))
|
||||
if metrics["over_limit"]["total"] > base["over_limit"]["total"] or total > limit:
|
||||
blocked.add(tuple(points[k - 1 : k + 3]))
|
||||
continue
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -40,9 +40,9 @@ def runs() -> dict:
|
||||
caught: dict = {}
|
||||
real = engine.build_pool
|
||||
|
||||
def spy(graph, follow, max_grade, min_radius_m, measure, snap):
|
||||
def spy(graph, follow, max_grade, min_radius_m, measure, snap, **kw):
|
||||
caught["measure"] = measure
|
||||
caught["pool"] = real(graph, follow, max_grade, min_radius_m, measure, snap)
|
||||
caught["pool"] = real(graph, follow, max_grade, min_radius_m, measure, snap, **kw)
|
||||
return caught["pool"]
|
||||
|
||||
engine._POOLS.clear()
|
||||
|
||||
@@ -17,6 +17,7 @@ from B05_Profile.B05_Profile_Engine_RouteInitial import ends_grade, generate_ini
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Merge import (
|
||||
TOTAL_EARTH_TOL,
|
||||
Guard,
|
||||
RouteEarth,
|
||||
merge_same_side,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Pool import SHORTLIST, choose, hard_limits
|
||||
@@ -113,7 +114,7 @@ def _merge(earth_base: float, delta: float):
|
||||
outline = build_planned_polyline(
|
||||
SAME, min_radius_m=RADIUS, simplify=False, curve_flags=[True] * len(SAME)
|
||||
)
|
||||
base = {**FLAT, "cut_m3": earth_base, "fill_m3": 0.0}
|
||||
base = {**FLAT, "cut_m3": 0.0, "fill_m3": 0.0} # 띠 가늠은 안 씀
|
||||
calls = []
|
||||
|
||||
def cross(before, after, window):
|
||||
@@ -121,7 +122,8 @@ def _merge(earth_base: float, delta: float):
|
||||
return delta
|
||||
|
||||
guard = Guard(SearchGrid(SAME, _flat_terrain(402.0, 262.0), [], stream_offset_m=100.0), 0.14)
|
||||
merged = merge_same_side((outline, base), guard, RADIUS, lambda line: base, cross)[0]
|
||||
earth = RouteEarth(lambda line: earth_base, cross)
|
||||
merged = merge_same_side((outline, base), guard, RADIUS, lambda line: base, earth)[0]
|
||||
return merged, calls
|
||||
|
||||
|
||||
@@ -148,22 +150,31 @@ def _route(name: str, earth: float, station: int = 0):
|
||||
|
||||
|
||||
def test_choose_profiles_only_shortlist() -> None:
|
||||
"""종단 반영 켬 토공 = 상한 누른 계획선 토공 위 3 개만 균형 종단 · 그 사이에서 고름."""
|
||||
routes = [_route(f"r{i}", 100.0 + 10 * i) for i in range(8)] + [_route("off", 1.0, 1)]
|
||||
"""종단 반영 켬 토공 = 노선 횡단 토공(줄 세움) 위 `SHORTLIST` 개만 균형 종단 · 그 사이에서 고름 ·
|
||||
줄 세움 = `earth`(켬 = 상한 누른 계획선 · 끔 = 지반고) · 없으면 띠 가늠."""
|
||||
routes = [_route(f"r{i}", 100.0 + 10 * i) for i in range(12)] + [_route("off", 1.0, 1)]
|
||||
last = f"r{SHORTLIST - 1}"
|
||||
profiled = []
|
||||
|
||||
def profile(route):
|
||||
profiled.append(route[1]["name"])
|
||||
# 균형 종단 토공은 순서가 바뀜 — 셋째가 가장 작음
|
||||
earth = {"r0": 900.0, "r1": 800.0, "r2": 300.0}.get(route[1]["name"], 50.0)
|
||||
# 균형 종단 토공은 순서가 바뀜 — 줄 세운 마지막이 가장 작음 · 밖은 더 작아도 안 봄
|
||||
earth = 300.0 if route[1]["name"] == last else 900.0
|
||||
route[1]["profile"] = {"cut_m3": earth, "fill_m3": 0, "borrow_m3": 0, "spoil_m3": 0}
|
||||
|
||||
chosen = choose("earthwork", routes, True, profile)
|
||||
assert profiled == ["r0", "r1", "r2"] and len(profiled) == SHORTLIST
|
||||
assert chosen[1]["name"] == "r2"
|
||||
assert profiled == [f"r{i}" for i in range(SHORTLIST)]
|
||||
assert chosen[1]["name"] == last
|
||||
# 종단 끔 = 줄 세우기 값 그대로 · 종단 안 붙임
|
||||
profiled.clear()
|
||||
assert choose("earthwork", routes, False, profile)[1]["name"] == "r0" and not profiled
|
||||
# 횡단 줄 세움이 띠 가늠과 다르면 그것을 따름(끔 = 지반고 횡단 하나로 고름)
|
||||
cross = {f"r{i}": 1000.0 - i for i in range(12)}
|
||||
|
||||
def earth(route, capped):
|
||||
return cross.get(route[1]["name"], 0.0) + (0.0 if capped else 5.0)
|
||||
|
||||
assert choose("earthwork", routes, False, None, earth)[1]["name"] == "r11"
|
||||
|
||||
|
||||
def test_ends_grade() -> None:
|
||||
|
||||
Reference in New Issue
Block a user