fix(B05 노선 추천 측점 맞춤): 합친 꼴 먼저 판에 측점 밖 생기면 옛 차례 판과 견줌 · 추천 창 설명 · ALGO_VERSION 7 (406-4)
- 측점 맞춤 — 같은 쪽 합친 꼴부터 놓다 뒤 곡선이 측점을 잃으면 옛 차례(그대로 → 합침) 판도 맞춰 측점 밖 적은 쪽 · 같으면 곡선 적은 쪽 - 검증_sub4 다섯 갈래 측점 밖 ≤ v5(weighted 5 → 1) - 추천 창 「측점 맞춤」 설명 — 6m 한도 문구를 지금 규칙(같은 쪽 합침 · 거리 한도 없음 · 복도 · 계류 · 기울기 · 토공)으로 - 시험 test_406_1 — 측점에 놓을 수 없는 쌍 안 합침 · 언덕 세 갈래 다른 선 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -26,6 +26,7 @@ R = 일반측점이 기본 제약(383-1 · 요구 241-13) — 추천 후보마
|
||||
· 잼을 지나는 놓기가 없으면 덜 나쁜 놓기(복도 안 · 더 넘는 기울기 `SOFT_SLACKS` 단계까지) —
|
||||
MC 를 다 측점에 놓는 첫 단계 판.
|
||||
· 그래도 못 놓으면 더 안 넘기는 놓기만 쓰고 그 곡선은 그 자리(측점 밖 · 기울기 먼저).
|
||||
· 합친 꼴 먼저 판에 측점 밖이 생기면 옛 차례(그대로 → 합침) 판도 맞춰 측점 밖이 적은 쪽(406-4).
|
||||
예상노선 추적 · 새 초기 노선은 안 줌(이탈 한도 그대로).
|
||||
|
||||
고른 값은 편집 갈래(`simplify=False` · 곡선 켬 · `radii`)로 다시 짠다 — 화면이 곡선을 교각점
|
||||
@@ -317,6 +318,7 @@ class _Snapper:
|
||||
self.guard = guard
|
||||
self.keep = False
|
||||
self.slack = math.inf # 못 지나는 놓기가 더 넘겨도 되는 기울기(`SOFT_SLACKS`)
|
||||
self.merged_first = guard is not None # 같은 쪽으로 합친 꼴부터 놓아 봄(406)
|
||||
|
||||
def _steeper(self, local: list[Xy]) -> bool:
|
||||
"""맞춘 조각이 원래 선 같은 자리보다 상한을 더 넘나(직접 잼이면 그 잼 전부)."""
|
||||
@@ -418,15 +420,16 @@ class _Snapper:
|
||||
break
|
||||
variants.append(trial)
|
||||
if self.guard is not None:
|
||||
# 직접 잼(406) — 같은 쪽으로 가장 많이 합친 꼴부터(S자 뺌 · 남은 꼴은 옛 차례) · 잼
|
||||
# 지나는 놓기를 모두 본 뒤 덜 나쁜 놓기(MC 는 늘 측점에)
|
||||
# 직접 잼(406) — 같은 쪽으로 가장 많이 합친 꼴부터(`merged_first` · S자 뺌 · 남은 꼴은
|
||||
# 옛 차례) · 잼 지나는 놓기를 모두 본 뒤 덜 나쁜 놓기(MC 는 늘 측점에)
|
||||
same = 1
|
||||
while same < len(variants):
|
||||
p = variants[same - 1].points
|
||||
if _turn_sign(*p[k - 1 : k + 2]) != _turn_sign(*p[k : k + 3]):
|
||||
break
|
||||
same += 1
|
||||
variants = variants[:same][::-1] + variants[same:]
|
||||
if self.merged_first:
|
||||
variants = variants[:same][::-1] + variants[same:]
|
||||
for soft in (False, True) if self.guard is not None else (False,):
|
||||
for trial in variants:
|
||||
yield from self._placements(state, trial, soft)
|
||||
@@ -567,9 +570,23 @@ def snap_curves_to_stations(
|
||||
"""
|
||||
if not outline.curves:
|
||||
return outline
|
||||
snapper = _Snapper(outline, min_radius_m, max_gap_m, excess, guard)
|
||||
snapped = _snap(_Snapper(outline, min_radius_m, max_gap_m, excess, guard), min_radius_m)
|
||||
if guard is not None and (snapped is None or off_station_count(snapped) > 0):
|
||||
# 합친 꼴부터 놓다 뒤 곡선이 측점을 잃으면 옛 차례(그대로 → 합침)로도 맞춰 측점 밖이 적은
|
||||
# 쪽 · 같으면 곡선이 적은 쪽(406-4 · 합친 곡선 때문에 측점 밖이 늘지 않게)
|
||||
plain = _Snapper(outline, min_radius_m, max_gap_m, excess, guard)
|
||||
plain.merged_first = False
|
||||
found = [line for line in (snapped, _snap(plain, min_radius_m)) if line is not None]
|
||||
snapped = min(
|
||||
found, key=lambda line: (off_station_count(line), len(line.curves)), default=None
|
||||
)
|
||||
return snapped
|
||||
|
||||
|
||||
def _snap(snapper: _Snapper, min_radius_m: float) -> PlannedPolyline | None:
|
||||
"""다 놓는 길(직접 잼이면 `SOFT_SLACKS` 단계) → 부분 맞춤 차례로 맞춘 선 · 못 하면 None."""
|
||||
state = None
|
||||
for slack in SOFT_SLACKS if guard is not None else (math.inf,):
|
||||
for slack in SOFT_SLACKS if snapper.guard is not None else (math.inf,):
|
||||
snapper.slack = slack
|
||||
state = state or snapper.run(keep=False)
|
||||
snapper.slack = 0.0
|
||||
|
||||
@@ -33,8 +33,8 @@ from B05_Profile.B05_Profile_Engine_RouteInitial import (
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
|
||||
# 3 = 곡선 MC 일반측점(351-2) · 4 = 다투는 곡선 끔(364-5) · 5 = 후보마다 맞춤(383-1) ·
|
||||
# 6 = 같은 쪽 곡선 합치기 · 거리 한도 대신 직접 잼(406)
|
||||
ALGO_VERSION = 6
|
||||
# 6 = 같은 쪽 곡선 합치기 · 거리 한도 대신 직접 잼(406) · 7 = 측점 밖이 늘면 옛 차례 맞춤(406-4)
|
||||
ALGO_VERSION = 7
|
||||
STORE_DIR = "B05_Profile/route_recommend"
|
||||
# 종류(갈래 · 지형) 마다 남길 벌 수 — 꺼낼 때 수정시각을 새로 해 최근에 쓴 것이 남음
|
||||
KEEP_PER_KIND = 2
|
||||
|
||||
@@ -103,7 +103,7 @@ const CRITERIA_HELP: Array<{ name: string; text: string }> = [
|
||||
},
|
||||
{
|
||||
name: "측점 맞춤",
|
||||
text: "켬(기본) = 곡선 가운데(MC)를 일반측점(20m) 위로 옮긴 선 — 교각점을 6m 안에서 밀고 반지름을 맞추며, 측점이 모자라면 곡선을 합침. 못 옮긴 곡선은 그 자리(「측점 밖 곡선 N개」). 비교값은 옮긴 선으로 다시 셈. 끔 = 옮기기 전 선. [적용]은 보이는 쪽",
|
||||
text: "켬(기본) = 곡선 가운데(MC)를 일반측점(20m) 위로 옮긴 선 — 교각점을 밀고 반지름을 맞추며, 같은 쪽으로 도는 이웃 곡선은 큰 곡선 하나로 합침(S자 사이 직선은 남김). 미는 거리 한도는 없고 탐색 복도 · 계류 이격 · 기울기 · 토공이 나빠지지 않는 자리만(예상노선 추적은 6m 안). 못 옮긴 곡선은 그 자리(「측점 밖 곡선 N개」). 비교값은 옮긴 선으로 다시 셈. 끔 = 옮기기 전 선. [적용]은 보이는 쪽",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
· 같은 쪽으로 도는 이웃 곡선 둘 → 바깥 두 직선 교점 하나(곡선 하나) · S자는 안 합침.
|
||||
· 막힘 — 지반 기울기(골짜기) · 복도 밖(지형 없음) · 계류 버퍼 안 나란히 · 노선 전체 잣대.
|
||||
· 큰 R(`widen`) 직접 잼 = 평지면 이탈 한도(4 m) 판보다 큼.
|
||||
· 406-4 — 측점에 놓을 수 없는 쌍은 처음부터 안 합침 · 언덕에서 세 갈래가 다른 선.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from shapely.geometry import LineString, Point
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial import generate_initial_route
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Merge import (
|
||||
GRADE_NOISE,
|
||||
Guard,
|
||||
@@ -122,3 +124,42 @@ def test_widen_guard_larger_than_gap_limit() -> None:
|
||||
gap = widen(fitted, path, RADIUS)
|
||||
direct = widen(fitted, path, RADIUS, guard=_guard(path))
|
||||
assert direct.curves[0].radius_m > gap.curves[0].radius_m + 20.0
|
||||
|
||||
|
||||
def test_406_4_unplaceable_pair_stays() -> None:
|
||||
"""합친 곡선 자리만 지형이 있음(그 둘레 3 m) — 가운데 꼴은 지나도 IP 를 밀어 측점에 놓은 꼴이
|
||||
복도 밖이면 처음부터 안 합침(합친 뒤 푸는 것 아님)."""
|
||||
merged = _merge(SAME, _guard(SAME))
|
||||
keep = [LineString(merged.vertices), LineString(_outline(SAME).vertices)]
|
||||
near = np.vectorize(lambda px, py: min(line.distance(Point(px, py)) for line in keep))
|
||||
guard = _guard(SAME, valid=lambda xx, yy: near(xx, yy) <= 3.0)
|
||||
cross = _line_intersection(*SAME)
|
||||
piece = guard.piece(SAME[0], cross, SAME[-1], merged.curves[0].radius_m)
|
||||
assert guard._look(piece)[3] # 가운데 꼴은 복도 안
|
||||
assert len(_merge(SAME, guard).curves) == 2
|
||||
|
||||
|
||||
def test_406_4_branches_differ_on_hill() -> None:
|
||||
"""제약을 지키는 길이 있는 언덕 — 영선 · 토공 최소 · 기울기 최소가 다른 선(후보 거름 뒤에도)."""
|
||||
x = np.arange(0.0, 802.0, 2.0)
|
||||
y = np.arange(0.0, 402.0, 2.0)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
hill = Terrain(
|
||||
x,
|
||||
y,
|
||||
40.0 * np.exp(-((xx - 400.0) ** 2 + (yy - 200.0) ** 2) / 90.0**2),
|
||||
np.ones(xx.shape, dtype=bool),
|
||||
)
|
||||
line = [(float(v), 200.0) for v in np.arange(50.0, 752.0, 4.0)]
|
||||
lines = {
|
||||
mode: LineString(
|
||||
generate_initial_route(
|
||||
line, hill, [], mode=mode, weights=None, max_grade=GRADE, min_radius_m=RADIUS
|
||||
)["planned"]
|
||||
)
|
||||
for mode in ("gradeline", "earthwork", "gentle")
|
||||
}
|
||||
names = list(lines)
|
||||
for i, a in enumerate(names):
|
||||
for b in names[i + 1 :]:
|
||||
assert lines[a].hausdorff_distance(lines[b]) > 10.0, (a, b)
|
||||
|
||||
Reference in New Issue
Block a user