- 못 지킨 곳 수 = 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
167 lines
7.6 KiB
Python
167 lines
7.6 KiB
Python
"""406 추천 초기노선 같은 쪽 곡선 합치기 · 직접 잼 — `B05_Profile_Engine_RouteInitial_Merge`.
|
|
|
|
./venv/Scripts/python.exe -m pytest resources/tester/test_406_1_route_merge.py -q
|
|
|
|
· 같은 쪽으로 도는 이웃 곡선 둘 → 바깥 두 직선 교점 하나(곡선 하나) · 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 Guard, merge_same_side
|
|
from B05_Profile.B05_Profile_Engine_RouteInitial_Radius import _flat, widen
|
|
from B05_Profile.B05_Profile_Engine_RouteInitial_Search import SearchGrid, Terrain
|
|
from B05_Profile.B05_Profile_Engine_RouteInitial_Station import _Reference
|
|
from common_util.common_util_route_polyline import _line_intersection, build_planned_polyline
|
|
|
|
RADIUS = 12.0
|
|
GRADE = 0.14
|
|
SAME = [(20.0, 20.0), (150.0, 20.0), (230.0, 70.0), (260.0, 180.0)] # 둘 다 왼쪽으로 돎
|
|
S_CURVE = [(20.0, 20.0), (150.0, 20.0), (230.0, 70.0), (360.0, 80.0)] # 왼쪽 → 오른쪽
|
|
FLAT = {"over_limit": {"total": 0, "grade": 0}, "max_grade_pct": 0.0, "cut_m3": 0.0, "fill_m3": 0.0}
|
|
|
|
|
|
def _outline(points):
|
|
return build_planned_polyline(
|
|
points, min_radius_m=RADIUS, simplify=False, curve_flags=[True] * len(points)
|
|
)
|
|
|
|
|
|
def _guard(points, z=None, valid=None, streams=(), stream_offset_m=100.0):
|
|
x = np.arange(0.0, 402.0, 2.0)
|
|
y = np.arange(0.0, 262.0, 2.0)
|
|
xx, yy = np.meshgrid(x, y)
|
|
terrain = Terrain(
|
|
x,
|
|
y,
|
|
np.zeros(xx.shape) if z is None else z(xx, yy),
|
|
np.ones(xx.shape, dtype=bool) if valid is None else valid(xx, yy),
|
|
)
|
|
grid = SearchGrid(points, terrain, list(streams), stream_offset_m=stream_offset_m)
|
|
return Guard(grid, GRADE)
|
|
|
|
|
|
def _distance(outline):
|
|
line = LineString(outline.vertices)
|
|
return np.vectorize(lambda px, py: line.distance(Point(px, py)))
|
|
|
|
|
|
def _merge(points, guard, measure=lambda line: FLAT):
|
|
outline = _outline(points)
|
|
return merge_same_side((outline, FLAT), guard, RADIUS, measure)[0]
|
|
|
|
|
|
def test_same_side_pair_merges_to_outer_intersection() -> None:
|
|
merged = _merge(SAME, _guard(SAME))
|
|
assert len(merged.curves) == 1
|
|
cross = _line_intersection(*SAME)
|
|
assert merged.curves[0].apex == (round(cross[0], 4), round(cross[1], 4))
|
|
assert merged.curves[0].radius_m > RADIUS
|
|
|
|
|
|
def test_s_curve_keeps_straight() -> None:
|
|
merged = _merge(S_CURVE, _guard(S_CURVE))
|
|
assert len(merged.curves) == 2
|
|
assert _flat(merged)[0] == _flat(_outline(S_CURVE))[0]
|
|
|
|
|
|
def test_valley_earth_blocks() -> None:
|
|
"""옛 선 = 골짜기 바닥 · 합친 곡선은 옆 비탈을 올라 토공 가늠이 늘어 막힘(419 — 원지반 기울기는
|
|
참고만이라 기울기로는 안 막음 · 넘는 만큼은 잼에 남음)."""
|
|
distance = _distance(_outline(SAME))
|
|
guard = _guard(SAME, z=lambda xx, yy: 3.0 * distance(xx, yy))
|
|
assert len(_merge(SAME, guard).curves) == 2
|
|
cross = _line_intersection(*SAME)
|
|
piece = guard.piece(SAME[0], cross, SAME[-1], 40.0)
|
|
window = _Reference(_outline(SAME).vertices).window(piece)
|
|
passed, (out, along, earth) = guard.judge(piece, window)
|
|
assert not passed and not out and along == 0.0 and earth > 0.0
|
|
assert guard._look(piece)[0] > guard._look(window)[0] # 기울기는 참고로 잼
|
|
|
|
|
|
def test_outside_corridor_blocks() -> None:
|
|
distance = _distance(_outline(SAME))
|
|
guard = _guard(SAME, valid=lambda xx, yy: distance(xx, yy) <= 4.0)
|
|
assert len(_merge(SAME, guard).curves) == 2
|
|
|
|
|
|
def test_stream_buffer_blocks() -> None:
|
|
"""평지에서 합친 큰 곡선 가운데에 나란한 계류 — 그 곡선만 버퍼 안을 나란히 지남."""
|
|
merged = _merge(SAME, _guard(SAME))
|
|
arc = LineString(merged.vertices)
|
|
middle = arc.interpolate(arc.project(Point(_line_intersection(*SAME))))
|
|
ahead = arc.interpolate(arc.project(middle) + 1.0)
|
|
unit = np.subtract(ahead.coords[0], middle.coords[0])
|
|
stream = [tuple(np.add(middle.coords[0], unit * s)) for s in (-12.0, 12.0)]
|
|
gap = LineString(_outline(SAME).vertices).distance(middle)
|
|
assert gap > 5.0
|
|
guard = _guard(SAME, streams=[stream], stream_offset_m=0.5 * gap)
|
|
original = _outline(SAME).vertices
|
|
assert guard._look(original)[2] == 0.0 # 옛 선은 버퍼 밖
|
|
# 버퍼 안을 나란히 지나는 큰 R 은 막힘 — 버퍼를 비키는 작은 R 로 합치거나 안 합침
|
|
kept = _merge(SAME, guard)
|
|
assert len(kept.curves) == 2 or kept.curves[0].radius_m < merged.curves[0].radius_m
|
|
assert guard._look(kept.vertices)[2] == 0.0
|
|
|
|
|
|
def test_route_yardstick_blocks() -> None:
|
|
"""노선 전체 잣대(못 지킨 곳 · 절성토 +10 %)가 합치기 전보다 나빠지면 안 합침 · 최대 기울기는
|
|
참고만(419)."""
|
|
steeper = {**FLAT, "max_grade_pct": 5.0}
|
|
assert len(_merge(SAME, _guard(SAME), measure=lambda line: steeper).curves) == 1
|
|
over = {**FLAT, "over_limit": {"total": 1, "grade": 0}}
|
|
assert len(_merge(SAME, _guard(SAME), measure=lambda line: over).curves) == 2
|
|
|
|
|
|
def test_widen_guard_larger_than_gap_limit() -> None:
|
|
"""곧은 두 직선이 직각으로 만나는 탐색 경로 — 이탈 4 m 면 R 이 못 커짐 · 평지 직접 잼은 큼."""
|
|
path = [(float(x), 20.0) for x in np.arange(20.0, 220.0, 2.0)]
|
|
path += [(220.0, float(y)) for y in np.arange(20.0, 222.0, 2.0)]
|
|
fitted = build_planned_polyline(path, min_radius_m=RADIUS)
|
|
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)
|