diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Station.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Station.py index 64791c14a..34a059919 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Station.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Station.py @@ -14,6 +14,7 @@ · 못 놓으면 다음 꺾임과 합침(같은 쪽으로 돌면 두 접선 교점 · 아니면 다음 IP 를 뺌) → 그래도 안 되면 그 곡선을 끔(꺾임만) → 그래도 이탈 한도를 넘으면 실패. · 모두 놓는 길이 없으면 못 놓는 곡선만 그 자리에 두고 나머지는 맞춤(부분 · 「측점 밖 곡선 N개」). + 그 전에 이웃 곡선과 한 측점을 다투면 덜 꺾인 쪽을 끔 — 꺾임점째 빼거나 꺾임만(364-5). · 그것도 못 하면 None — 부르는 쪽이 원래 선을 씀. 고른 값은 편집 갈래(`simplify=False` · 곡선 켬 · `radii`)로 다시 짠다 — 화면이 곡선을 교각점 @@ -25,7 +26,7 @@ from __future__ import annotations import math from collections.abc import Callable, Iterator -from dataclasses import dataclass +from dataclasses import dataclass, replace import numpy as np import shapely @@ -362,11 +363,63 @@ class _Snapper: kink.flags[k], kink.radii[k] = False, None yield self._kink(state, kink) if self.keep: + yield from self._crowded(state) yield from self._kept(state) - def _kept(self, state: _State) -> Iterator[_State]: - """못 놓는 곡선은 그 자리 그대로(브레인 확정 · 측점 밖 곡선으로 셈).""" + def _crowded(self, state: _State) -> Iterator[_State]: + """이웃 곡선과 한 측점을 다툼(꺾임점 사이 20 m 안 · 364-5) — 덜 꺾인 쪽 곡선을 끔. + + 끈 꺾임이 원래 선에서 한도 안일 때만. 이 곡선을 끄면 꺾임만 · 뒤 곡선을 끄면 이 곡선을 + 다시 놓아 봄(못 놓으면 그 자리). 앞 곡선은 이미 정해져 이 곡선만 끔. + """ plan, k = state.plan, state.k + p = plan.points + + def close(i: int) -> bool: + return 1 <= i < len(p) - 1 and plan.flags[i] and math.dist(p[i], p[k]) < STATION_M + + pair = [k, k + 1] if close(k + 1) else [] + # 덜 꺾인 쪽부터 · 그걸 꺼도 못 놓으면 다른 쪽 + pair.sort(key=lambda i: -_inner_angle_deg(*p[i - 1 : i + 2])) + found = _try(state, plan, k, p[k], plan.radii[k] or self.min_radius) + # 앞 곡선 MC 측점에서 20 m 안(남는 측점 없음)도 다툼 + crowded = ( + found is not None + and 0 < state.last_station + and (found[1] - state.last_station < STATION_M) + ) + if (close(k - 1) or crowded) and k not in pair: + pair.append(k) + for small in pair: + a, at, b = p[small - 1 : small + 2] + chord = [ + (a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t) for t in (0.25, 0.5, 0.75) + ] + # 꺾임점째 뺌(이웃 곡선 접선 자리가 넓어짐) → 안 되면 꺾임만 남김. 이 꺾임점은 앞 + # 곡선이 꺾임만일 때만 뺌(앞 곡선 EC 가 이 꺾임점 쪽 직선 위) + movable = small > k or not plan.flags[k - 1] + if movable and self.reference.gap(chord) <= self.max_gap and not self._steeper(chord): + gone = plan.copy() + del gone.points[small], gone.flags[small], gone.radii[small] + yield replace(state, plan=gone) + piece = [ + ((a[0] + at[0]) / 2, (a[1] + at[1]) / 2), + at, + ((at[0] + b[0]) / 2, (at[1] + b[1]) / 2), + ] + if self.reference.gap(piece) > self.max_gap or self._steeper(piece): + continue + off = plan.copy() + off.flags[small], off.radii[small] = False, None + if small == k: + yield self._kink(state, off) + else: + yield from self._placements(state, off) + yield from self._kept(state, off) + + def _kept(self, state: _State, plan: _Plan | None = None) -> Iterator[_State]: + """못 놓는 곡선은 그 자리 그대로(브레인 확정 · 측점 밖 곡선으로 셈).""" + plan, k = plan or state.plan, state.k found = _try(state, plan, k, plan.points[k], plan.radii[k] or self.min_radius) if found is None: return diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py index 878f69139..8b89d8468 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py @@ -32,7 +32,7 @@ from B05_Profile.B05_Profile_Engine_RouteInitial import ( ) from common_util.common_util_json import atomic_write_json -ALGO_VERSION = 3 # 3 = 곡선 MC 일반측점(351-2) +ALGO_VERSION = 4 # 3 = 곡선 MC 일반측점(351-2) · 4 = 다투는 곡선 끔(364-5) STORE_DIR = "B05_Profile/route_recommend" # 종류(갈래 · 지형) 마다 남길 벌 수 — 꺼낼 때 수정시각을 새로 해 최근에 쓴 것이 남음 KEEP_PER_KIND = 2 diff --git a/resources/tester/fixtures/b05_364_5_unsnapped_req239.json b/resources/tester/fixtures/b05_364_5_unsnapped_req239.json new file mode 100644 index 000000000..edd99abc7 --- /dev/null +++ b/resources/tester/fixtures/b05_364_5_unsnapped_req239.json @@ -0,0 +1,1287 @@ +[ +{ +"mode": "gradeline", +"criteria": { +"max_grade_pct": 14.0, +"min_radius_m": 12.0, +"stream_offset_m": 100.0, +"road_width_m": 4.0, +"grade_smooth_m": 5.0 +}, +"nodes": [ +{ +"x": 208403.2001, +"y": 367920.7514, +"radius_m": null +}, +{ +"x": 208387.2031, +"y": 367887.8125, +"radius_m": 79.0 +}, +{ +"x": 208387.2031, +"y": 367797.7812, +"radius_m": 40.0 +}, +{ +"x": 208395.7812, +"y": 367780.625, +"radius_m": 59.0 +}, +{ +"x": 208412.9531, +"y": 367763.4688, +"radius_m": 29.0 +}, +{ +"x": 208421.5469, +"y": 367759.1875, +"radius_m": 20.0 +}, +{ +"x": 208485.9219, +"y": 367759.1875, +"radius_m": 40.0 +}, +{ +"x": 208503.0938, +"y": 367767.7812, +"radius_m": 36.0 +}, +{ +"x": 208520.25, +"y": 367767.7812, +"radius_m": 20.0 +}, +{ +"x": 208528.8438, +"y": 367763.4688, +"radius_m": 29.0 +}, +{ +"x": 208537.4219, +"y": 367754.9062, +"radius_m": 37.0 +}, +{ +"x": 208558.875, +"y": 367712.0312, +"radius_m": 74.0 +}, +{ +"x": 208576.0469, +"y": 367694.875, +"radius_m": 59.0 +}, +{ +"x": 208593.2188, +"y": 367686.3125, +"radius_m": 27.0 +}, +{ +"x": 208606.0938, +"y": 367686.3125, +"radius_m": 27.0 +}, +{ +"x": 208640.4219, +"y": 367703.4688, +"radius_m": 93.0 +}, +{ +"x": 208661.8906, +"y": 367724.9062, +"radius_m": 93.0 +}, +{ +"x": 208696.2188, +"y": 367793.5, +"radius_m": 55.0 +}, +{ +"x": 208709.0938, +"y": 367806.3438, +"radius_m": 29.0 +}, +{ +"x": 208717.6875, +"y": 367810.6562, +"radius_m": 18.0 +}, +{ +"x": 208726.2656, +"y": 367810.6562, +"radius_m": 18.0 +}, +{ +"x": 208734.8594, +"y": 367806.3438, +"radius_m": 20.0 +}, +{ +"x": 208756.3125, +"y": 367806.3438, +"radius_m": 45.0 +}, +{ +"x": 208799.2344, +"y": 367827.7812, +"radius_m": 147.0 +}, +{ +"x": 208833.5625, +"y": 367862.0938, +"radius_m": 29.0 +}, +{ +"x": 208837.8594, +"y": 367870.6562, +"radius_m": 29.0 +}, +{ +"x": 208846.4375, +"y": 367879.25, +"radius_m": 29.0 +}, +{ +"x": 208855.0312, +"y": 367883.5312, +"radius_m": 18.0 +}, +{ +"x": 208863.6094, +"y": 367883.5312, +"radius_m": 18.0 +}, +{ +"x": 208889.3594, +"y": 367870.6562, +"radius_m": 18.0 +}, +{ +"x": 208897.9531, +"y": 367870.6562, +"radius_m": 18.0 +}, +{ +"x": 208923.7031, +"y": 367883.5312, +"radius_m": 37.0 +}, +{ +"x": 208932.2812, +"y": 367892.0938, +"radius_m": 37.0 +}, +{ +"x": 208940.875, +"y": 367909.25, +"radius_m": 37.0 +}, +{ +"x": 208949.4531, +"y": 367917.8438, +"radius_m": 29.0 +}, +{ +"x": 208958.0312, +"y": 367922.125, +"radius_m": 18.0 +}, +{ +"x": 208966.625, +"y": 367922.125, +"radius_m": 18.0 +}, +{ +"x": 208975.2031, +"y": 367917.8438, +"radius_m": 29.0 +}, +{ +"x": 208983.7812, +"y": 367909.25, +"radius_m": 29.0 +}, +{ +"x": 208988.0781, +"y": 367900.6875, +"radius_m": 29.0 +}, +{ +"x": 209000.9531, +"y": 367887.8125, +"radius_m": 29.0 +}, +{ +"x": 209005.25, +"y": 367879.25, +"radius_m": 18.0 +}, +{ +"x": 209005.25, +"y": 367870.6562, +"radius_m": 18.0 +}, +{ +"x": 208988.0781, +"y": 367836.375, +"radius_m": 63.0 +}, +{ +"x": 208988.0781, +"y": 367806.3438, +"radius_m": 40.0 +}, +{ +"x": 208996.6562, +"y": 367789.2188, +"radius_m": 59.0 +}, +{ +"x": 209018.125, +"y": 367767.7812, +"radius_m": 88.0 +}, +{ +"x": 209031.0, +"y": 367742.0312, +"radius_m": 27.0 +}, +{ +"x": 209031.0, +"y": 367729.1875, +"radius_m": 27.0 +}, +{ +"x": 209022.4219, +"y": 367712.0312, +"radius_m": 18.0 +}, +{ +"x": 209022.4219, +"y": 367703.4688, +"radius_m": 18.0 +}, +{ +"x": 209026.7031, +"y": 367694.875, +"radius_m": 46.0 +}, +{ +"x": 209038.0798, +"y": 367680.426, +"radius_m": null +} +], +"old": { +"off": 5, +"curves": 34 +} +}, +{ +"mode": "weighted", +"criteria": { +"max_grade_pct": 14.0, +"min_radius_m": 12.0, +"stream_offset_m": 100.0, +"road_width_m": 4.0, +"grade_smooth_m": 5.0 +}, +"nodes": [ +{ +"x": 208403.2001, +"y": 367920.7514, +"radius_m": null +}, +{ +"x": 208404.375, +"y": 367844.9375, +"radius_m": 83.0 +}, +{ +"x": 208421.5469, +"y": 367810.6562, +"radius_m": 74.0 +}, +{ +"x": 208438.7031, +"y": 367793.5, +"radius_m": 29.0 +}, +{ +"x": 208447.2969, +"y": 367789.2188, +"radius_m": 20.0 +}, +{ +"x": 208515.9688, +"y": 367789.2188, +"radius_m": 40.0 +}, +{ +"x": 208533.125, +"y": 367797.7812, +"radius_m": 18.0 +}, +{ +"x": 208541.7188, +"y": 367797.7812, +"radius_m": 18.0 +}, +{ +"x": 208550.2969, +"y": 367793.5, +"radius_m": 29.0 +}, +{ +"x": 208558.875, +"y": 367784.9062, +"radius_m": 37.0 +}, +{ +"x": 208576.0469, +"y": 367750.625, +"radius_m": 74.0 +}, +{ +"x": 208593.2188, +"y": 367733.4688, +"radius_m": 29.0 +}, +{ +"x": 208601.7969, +"y": 367729.1875, +"radius_m": 18.0 +}, +{ +"x": 208610.3906, +"y": 367729.1875, +"radius_m": 18.0 +}, +{ +"x": 208636.1406, +"y": 367742.0312, +"radius_m": 74.0 +}, +{ +"x": 208653.3125, +"y": 367759.1875, +"radius_m": 74.0 +}, +{ +"x": 208674.7656, +"y": 367802.0625, +"radius_m": 56.0 +}, +{ +"x": 208687.6406, +"y": 367814.9375, +"radius_m": 56.0 +}, +{ +"x": 208696.2188, +"y": 367832.0938, +"radius_m": 37.0 +}, +{ +"x": 208704.8125, +"y": 367840.6562, +"radius_m": 29.0 +}, +{ +"x": 208713.3906, +"y": 367844.9375, +"radius_m": 18.0 +}, +{ +"x": 208721.9844, +"y": 367844.9375, +"radius_m": 18.0 +}, +{ +"x": 208739.1406, +"y": 367836.375, +"radius_m": 40.0 +}, +{ +"x": 208760.6094, +"y": 367836.375, +"radius_m": 45.0 +}, +{ +"x": 208803.5312, +"y": 367857.8125, +"radius_m": 147.0 +}, +{ +"x": 208837.8594, +"y": 367892.0938, +"radius_m": 29.0 +}, +{ +"x": 208846.4375, +"y": 367896.4062, +"radius_m": 18.0 +}, +{ +"x": 208855.0312, +"y": 367896.4062, +"radius_m": 18.0 +}, +{ +"x": 208889.3594, +"y": 367879.25, +"radius_m": 27.0 +}, +{ +"x": 208902.2344, +"y": 367879.25, +"radius_m": 27.0 +}, +{ +"x": 208919.4062, +"y": 367887.8125, +"radius_m": 37.0 +}, +{ +"x": 208927.9844, +"y": 367896.4062, +"radius_m": 37.0 +}, +{ +"x": 208936.5781, +"y": 367913.5312, +"radius_m": 37.0 +}, +{ +"x": 208945.1562, +"y": 367922.125, +"radius_m": 29.0 +}, +{ +"x": 208953.75, +"y": 367926.4062, +"radius_m": 18.0 +}, +{ +"x": 208962.3281, +"y": 367926.4062, +"radius_m": 18.0 +}, +{ +"x": 208970.9062, +"y": 367922.125, +"radius_m": 29.0 +}, +{ +"x": 208979.5, +"y": 367913.5312, +"radius_m": 29.0 +}, +{ +"x": 208983.7812, +"y": 367904.9688, +"radius_m": 29.0 +}, +{ +"x": 209000.9531, +"y": 367887.8125, +"radius_m": 29.0 +}, +{ +"x": 209005.25, +"y": 367879.25, +"radius_m": 18.0 +}, +{ +"x": 209005.25, +"y": 367870.6562, +"radius_m": 18.0 +}, +{ +"x": 208988.0781, +"y": 367836.375, +"radius_m": 72.0 +}, +{ +"x": 208988.0781, +"y": 367802.0625, +"radius_m": 20.0 +}, +{ +"x": 208992.375, +"y": 367793.5, +"radius_m": 29.0 +}, +{ +"x": 209018.125, +"y": 367767.7812, +"radius_m": 88.0 +}, +{ +"x": 209031.0, +"y": 367742.0312, +"radius_m": 36.0 +}, +{ +"x": 209031.0, +"y": 367724.9062, +"radius_m": 20.0 +}, +{ +"x": 209026.7031, +"y": 367716.3125, +"radius_m": 18.0 +}, +{ +"x": 209026.7031, +"y": 367707.75, +"radius_m": 21.0 +}, +{ +"x": 209038.0798, +"y": 367680.426, +"radius_m": null +} +], +"old": { +"off": 3, +"curves": 35 +} +}, +{ +"mode": "gentle", +"criteria": { +"max_grade_pct": 14.0, +"min_radius_m": 12.0, +"stream_offset_m": 100.0, +"road_width_m": 4.0, +"grade_smooth_m": 5.0 +}, +"nodes": [ +{ +"x": 208403.2001, +"y": 367920.7514, +"radius_m": null +}, +{ +"x": 208391.5, +"y": 367896.4062, +"radius_m": 59.0 +}, +{ +"x": 208391.5, +"y": 367797.7812, +"radius_m": 40.0 +}, +{ +"x": 208400.0781, +"y": 367780.625, +"radius_m": 59.0 +}, +{ +"x": 208417.25, +"y": 367763.4688, +"radius_m": 29.0 +}, +{ +"x": 208425.8281, +"y": 367759.1875, +"radius_m": 20.0 +}, +{ +"x": 208481.625, +"y": 367759.1875, +"radius_m": 60.0 +}, +{ +"x": 208507.375, +"y": 367772.0625, +"radius_m": 36.0 +}, +{ +"x": 208524.5469, +"y": 367772.0625, +"radius_m": 20.0 +}, +{ +"x": 208533.125, +"y": 367767.7812, +"radius_m": 29.0 +}, +{ +"x": 208541.7188, +"y": 367759.1875, +"radius_m": 37.0 +}, +{ +"x": 208563.1719, +"y": 367716.3125, +"radius_m": 93.0 +}, +{ +"x": 208584.6406, +"y": 367694.875, +"radius_m": 29.0 +}, +{ +"x": 208593.2188, +"y": 367690.5938, +"radius_m": 18.0 +}, +{ +"x": 208601.7969, +"y": 367690.5938, +"radius_m": 18.0 +}, +{ +"x": 208644.7188, +"y": 367712.0312, +"radius_m": 93.0 +}, +{ +"x": 208666.1875, +"y": 367733.4688, +"radius_m": 93.0 +}, +{ +"x": 208700.5156, +"y": 367802.0625, +"radius_m": 37.0 +}, +{ +"x": 208709.0938, +"y": 367810.6562, +"radius_m": 29.0 +}, +{ +"x": 208717.6875, +"y": 367814.9375, +"radius_m": 18.0 +}, +{ +"x": 208726.2656, +"y": 367814.9375, +"radius_m": 18.0 +}, +{ +"x": 208734.8594, +"y": 367810.6562, +"radius_m": 20.0 +}, +{ +"x": 208756.3125, +"y": 367810.6562, +"radius_m": 45.0 +}, +{ +"x": 208807.8125, +"y": 367836.375, +"radius_m": 112.0 +}, +{ +"x": 208833.5625, +"y": 367862.0938, +"radius_m": 29.0 +}, +{ +"x": 208837.8594, +"y": 367870.6562, +"radius_m": 29.0 +}, +{ +"x": 208846.4375, +"y": 367879.25, +"radius_m": 29.0 +}, +{ +"x": 208855.0312, +"y": 367883.5312, +"radius_m": 18.0 +}, +{ +"x": 208863.6094, +"y": 367883.5312, +"radius_m": 18.0 +}, +{ +"x": 208872.2031, +"y": 367879.25, +"radius_m": 29.0 +}, +{ +"x": 208880.7812, +"y": 367870.6562, +"radius_m": 29.0 +}, +{ +"x": 208889.3594, +"y": 367866.375, +"radius_m": 18.0 +}, +{ +"x": 208897.9531, +"y": 367866.375, +"radius_m": 18.0 +}, +{ +"x": 208923.7031, +"y": 367879.25, +"radius_m": 37.0 +}, +{ +"x": 208932.2812, +"y": 367887.8125, +"radius_m": 37.0 +}, +{ +"x": 208940.875, +"y": 367904.9688, +"radius_m": 37.0 +}, +{ +"x": 208949.4531, +"y": 367913.5312, +"radius_m": 29.0 +}, +{ +"x": 208958.0312, +"y": 367917.8438, +"radius_m": 18.0 +}, +{ +"x": 208966.625, +"y": 367917.8438, +"radius_m": 18.0 +}, +{ +"x": 208975.2031, +"y": 367913.5312, +"radius_m": 29.0 +}, +{ +"x": 208983.7812, +"y": 367904.9688, +"radius_m": 29.0 +}, +{ +"x": 208988.0781, +"y": 367896.4062, +"radius_m": 29.0 +}, +{ +"x": 208996.6562, +"y": 367887.8125, +"radius_m": 29.0 +}, +{ +"x": 209000.9531, +"y": 367879.25, +"radius_m": 18.0 +}, +{ +"x": 209000.9531, +"y": 367870.6562, +"radius_m": 18.0 +}, +{ +"x": 208983.7812, +"y": 367836.375, +"radius_m": 72.0 +}, +{ +"x": 208983.7812, +"y": 367802.0625, +"radius_m": 20.0 +}, +{ +"x": 208988.0781, +"y": 367793.5, +"radius_m": 29.0 +}, +{ +"x": 209000.9531, +"y": 367780.625, +"radius_m": 29.0 +}, +{ +"x": 209009.5312, +"y": 367776.3438, +"radius_m": 29.0 +}, +{ +"x": 209018.125, +"y": 367767.7812, +"radius_m": 37.0 +}, +{ +"x": 209031.0, +"y": 367742.0312, +"radius_m": 27.0 +}, +{ +"x": 209031.0, +"y": 367729.1875, +"radius_m": 27.0 +}, +{ +"x": 209022.4219, +"y": 367712.0312, +"radius_m": 18.0 +}, +{ +"x": 209022.4219, +"y": 367703.4688, +"radius_m": 18.0 +}, +{ +"x": 209026.7031, +"y": 367694.875, +"radius_m": 46.0 +}, +{ +"x": 209038.0798, +"y": 367680.426, +"radius_m": null +} +], +"old": { +"off": 0, +"curves": 33 +} +}, +{ +"mode": "follow", +"criteria": { +"max_grade_pct": 14.0, +"min_radius_m": 12.0, +"stream_offset_m": 100.0, +"road_width_m": 4.0, +"grade_smooth_m": 5.0 +}, +"nodes": [ +{ +"x": 208403.2001, +"y": 367920.7514, +"radius_m": null +}, +{ +"x": 208406.6653, +"y": 367838.8503, +"radius_m": 12.0 +}, +{ +"x": 208430.3373, +"y": 367783.6221, +"radius_m": 12.0 +}, +{ +"x": 208476.8219, +"y": 367786.7347, +"radius_m": 42.48 +}, +{ +"x": 208525.249, +"y": 367796.5535, +"radius_m": 51.04 +}, +{ +"x": 208541.6054, +"y": 367806.8906, +"radius_m": 12.0 +}, +{ +"x": 208551.4054, +"y": 367804.9001, +"radius_m": 12.0 +}, +{ +"x": 208559.8564, +"y": 367799.554, +"radius_m": 12.0 +}, +{ +"x": 208585.1399, +"y": 367745.143, +"radius_m": 12.0 +}, +{ +"x": 208598.2227, +"y": 367730.0154, +"radius_m": 12.0 +}, +{ +"x": 208618.2197, +"y": 367730.3725, +"radius_m": 12.0 +}, +{ +"x": 208648.5793, +"y": 367756.0745, +"radius_m": 12.0 +}, +{ +"x": 208660.1679, +"y": 367772.3751, +"radius_m": 12.0 +}, +{ +"x": 208680.2884, +"y": 367806.9411, +"radius_m": 161.2 +}, +{ +"x": 208691.8897, +"y": 367823.2327, +"radius_m": 198.88 +}, +{ +"x": 208708.0905, +"y": 367848.465, +"radius_m": 12.0 +}, +{ +"x": 208727.6231, +"y": 367852.2027, +"radius_m": 60.55 +}, +{ +"x": 208776.9786, +"y": 367845.4246, +"radius_m": 12.0 +}, +{ +"x": 208813.3157, +"y": 367862.0735, +"radius_m": 74.12 +}, +{ +"x": 208846.4161, +"y": 367898.9666, +"radius_m": 12.0 +}, +{ +"x": 208903.1678, +"y": 367883.1419, +"radius_m": 12.0 +}, +{ +"x": 208943.1297, +"y": 367913.014, +"radius_m": 18.13 +}, +{ +"x": 208971.5763, +"y": 367920.0899, +"radius_m": 12.0 +}, +{ +"x": 208996.6506, +"y": 367889.5967, +"radius_m": 12.0 +}, +{ +"x": 208982.2093, +"y": 367842.143, +"radius_m": 28.39 +}, +{ +"x": 208993.3725, +"y": 367803.7642, +"radius_m": 12.0 +}, +{ +"x": 209029.6063, +"y": 367756.3647, +"radius_m": 12.0 +}, +{ +"x": 209038.0798, +"y": 367680.426, +"radius_m": null +} +], +"old": { +"off": 0, +"curves": 25 +} +}, +{ +"mode": "earthwork", +"criteria": { +"max_grade_pct": 14.0, +"min_radius_m": 12.0, +"stream_offset_m": 100.0, +"road_width_m": 4.0, +"grade_smooth_m": 5.0 +}, +"nodes": [ +{ +"x": 208403.2001, +"y": 367920.7514, +"radius_m": null +}, +{ +"x": 208391.5, +"y": 367896.4062, +"radius_m": 59.0 +}, +{ +"x": 208391.5, +"y": 367797.7812, +"radius_m": 40.0 +}, +{ +"x": 208400.0781, +"y": 367780.625, +"radius_m": 59.0 +}, +{ +"x": 208417.25, +"y": 367763.4688, +"radius_m": 29.0 +}, +{ +"x": 208425.8281, +"y": 367759.1875, +"radius_m": 20.0 +}, +{ +"x": 208481.625, +"y": 367759.1875, +"radius_m": 60.0 +}, +{ +"x": 208507.375, +"y": 367772.0625, +"radius_m": 36.0 +}, +{ +"x": 208524.5469, +"y": 367772.0625, +"radius_m": 20.0 +}, +{ +"x": 208533.125, +"y": 367767.7812, +"radius_m": 29.0 +}, +{ +"x": 208541.7188, +"y": 367759.1875, +"radius_m": 37.0 +}, +{ +"x": 208563.1719, +"y": 367716.3125, +"radius_m": 93.0 +}, +{ +"x": 208584.6406, +"y": 367694.875, +"radius_m": 29.0 +}, +{ +"x": 208593.2188, +"y": 367690.5938, +"radius_m": 18.0 +}, +{ +"x": 208601.7969, +"y": 367690.5938, +"radius_m": 18.0 +}, +{ +"x": 208644.7188, +"y": 367712.0312, +"radius_m": 93.0 +}, +{ +"x": 208666.1875, +"y": 367733.4688, +"radius_m": 93.0 +}, +{ +"x": 208700.5156, +"y": 367802.0625, +"radius_m": 37.0 +}, +{ +"x": 208709.0938, +"y": 367810.6562, +"radius_m": 29.0 +}, +{ +"x": 208717.6875, +"y": 367814.9375, +"radius_m": 18.0 +}, +{ +"x": 208726.2656, +"y": 367814.9375, +"radius_m": 18.0 +}, +{ +"x": 208734.8594, +"y": 367810.6562, +"radius_m": 20.0 +}, +{ +"x": 208756.3125, +"y": 367810.6562, +"radius_m": 45.0 +}, +{ +"x": 208807.8125, +"y": 367836.375, +"radius_m": 112.0 +}, +{ +"x": 208833.5625, +"y": 367862.0938, +"radius_m": 29.0 +}, +{ +"x": 208837.8594, +"y": 367870.6562, +"radius_m": 29.0 +}, +{ +"x": 208846.4375, +"y": 367879.25, +"radius_m": 29.0 +}, +{ +"x": 208855.0312, +"y": 367883.5312, +"radius_m": 18.0 +}, +{ +"x": 208863.6094, +"y": 367883.5312, +"radius_m": 18.0 +}, +{ +"x": 208889.3594, +"y": 367870.6562, +"radius_m": 18.0 +}, +{ +"x": 208897.9531, +"y": 367870.6562, +"radius_m": 18.0 +}, +{ +"x": 208923.7031, +"y": 367883.5312, +"radius_m": 37.0 +}, +{ +"x": 208932.2812, +"y": 367892.0938, +"radius_m": 37.0 +}, +{ +"x": 208940.875, +"y": 367909.25, +"radius_m": 37.0 +}, +{ +"x": 208949.4531, +"y": 367917.8438, +"radius_m": 29.0 +}, +{ +"x": 208958.0312, +"y": 367922.125, +"radius_m": 18.0 +}, +{ +"x": 208966.625, +"y": 367922.125, +"radius_m": 18.0 +}, +{ +"x": 208975.2031, +"y": 367917.8438, +"radius_m": 29.0 +}, +{ +"x": 208983.7812, +"y": 367909.25, +"radius_m": 29.0 +}, +{ +"x": 208988.0781, +"y": 367900.6875, +"radius_m": 29.0 +}, +{ +"x": 209000.9531, +"y": 367887.8125, +"radius_m": 29.0 +}, +{ +"x": 209005.25, +"y": 367879.25, +"radius_m": 18.0 +}, +{ +"x": 209005.25, +"y": 367870.6562, +"radius_m": 18.0 +}, +{ +"x": 208988.0781, +"y": 367836.375, +"radius_m": 72.0 +}, +{ +"x": 208988.0781, +"y": 367802.0625, +"radius_m": 20.0 +}, +{ +"x": 208992.375, +"y": 367793.5, +"radius_m": 29.0 +}, +{ +"x": 209018.125, +"y": 367767.7812, +"radius_m": 88.0 +}, +{ +"x": 209031.0, +"y": 367742.0312, +"radius_m": 36.0 +}, +{ +"x": 209031.0, +"y": 367724.9062, +"radius_m": 20.0 +}, +{ +"x": 209026.7031, +"y": 367716.3125, +"radius_m": 18.0 +}, +{ +"x": 209026.7031, +"y": 367707.75, +"radius_m": 21.0 +}, +{ +"x": 209038.0798, +"y": 367680.426, +"radius_m": null +} +], +"old": { +"off": 8, +"curves": 39 +} +} +] \ No newline at end of file diff --git a/resources/tester/test_364_5_crowded_curves.py b/resources/tester/test_364_5_crowded_curves.py new file mode 100644 index 000000000..0dffac70c --- /dev/null +++ b/resources/tester/test_364_5_crowded_curves.py @@ -0,0 +1,150 @@ +"""364-5 한 측점을 다투는 가까운 두 곡선 — `B05_Profile_Engine_RouteInitial_Station._crowded`. + + ./venv/Scripts/python.exe -m pytest resources/tester/test_364_5_crowded_curves.py -q -s + +자료 = 브레인 검증 판(요구 239) 다섯 갈래의 맞추기 전 선(`fixtures/b05_364_5_unsnapped_req239.json`) ++ 검증_sub1 지형. 측점 밖 곡선 수 · 넘은 곳 · 최대 기울기를 맞추기 전 · 옛 맞춤 · 새 맞춤으로 찍고 +이웃 MC 20 m 안 측점 밖 쌍 0 · 측점 밖 합계가 옛 맞춤의 1/4 이하 · 이탈 한도 안을 봄. +""" + +import json +from pathlib import Path + +import numpy as np +import pytest +from scipy.interpolate import RegularGridInterpolator +from shapely.geometry import LineString + +from B05_Profile.B05_Profile_Engine_RouteInitial import Terrain +from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import ( + SAMPLE_M, + _resample, + route_metrics, + smoothed_ground, +) +from B05_Profile.B05_Profile_Engine_RouteInitial_Station import ( + MC_TOL_M, + STATION_M, + curve_mc_chainages, + off_station_count, + snap_curves_to_stations, +) +from common_util.common_util_route_geometry import read_planned_route_csv +from common_util.common_util_route_polyline import build_planned_polyline +from config.config_system_design import ROUTE_STATION_SNAP_MAX_GAP_M + +HERE = Path(__file__).resolve().parent +PROJECT = HERE.parents[1] / "storage/1/3/7e420ed6-f940-4d12-a1a5-39cdcf70a391" +SURFACE = PROJECT / "B05_Profile/route/cost_surface_classification_dtm_smooth.npz" +CASES = json.loads((HERE / "fixtures/b05_364_5_unsnapped_req239.json").read_text(encoding="utf-8")) + + +def _ls(vertices: list) -> LineString: + return LineString([xy for i, xy in enumerate(vertices) if not i or xy != vertices[i - 1]]) + + +def _outline(case: dict): + nodes = case["nodes"] + return build_planned_polyline( + [(n["x"], n["y"]) for n in nodes], + min_radius_m=case["criteria"]["min_radius_m"], + simplify=False, + curve_flags=[n["radius_m"] is not None for n in nodes], + radii=[n["radius_m"] for n in nodes], + ) + + +def _excess(terrain: Terrain, max_grade: float, smooth: float): + """`generate_initial_route.grade_excess` 와 같은 잣대(20 m 고른 지반 기울기 − 상한).""" + surface = RegularGridInterpolator( + (terrain.y, terrain.x), + smoothed_ground(terrain, smooth), + bounds_error=False, + fill_value=None, + ) + + def excess(points): + xy, chain = _resample(points, SAMPLE_M) + z = surface(xy[:, ::-1]) + span = min(int(round(STATION_M / SAMPLE_M)), len(z) - 1) + if span < 1: + return 0.0 + run = np.maximum(chain[span:] - chain[:-span], 1e-9) + return max(0.0, float((np.abs(z[span:] - z[:-span]) / run).max()) - max_grade) + + return excess + + +def _off_mcs(outline) -> list[float]: + return [ + mc + for mc in curve_mc_chainages(outline) + if abs(mc - STATION_M * round(mc / STATION_M)) > MC_TOL_M + ] + + +def test_req239_five_modes_before_after() -> None: + if not SURFACE.is_file(): + pytest.skip("검증_sub1 storage 없음") + cached = np.load(SURFACE, allow_pickle=False) + terrain = Terrain(cached["x"], cached["y"], cached["z"], cached["valid_mask"]) + expected = [ + (v.x, v.y) + for v in read_planned_route_csv(PROJECT / "B05_Profile/route/expected_route.csv").vertices + ] + old_total = new_total = 0 + for case in CASES: + cr = case["criteria"] + before = _outline(case) + follow = case["mode"] == "follow" + excess = ( + None if follow else _excess(terrain, cr["max_grade_pct"] / 100, cr["grade_smooth_m"]) + ) + after = snap_curves_to_stations(before, cr["min_radius_m"], excess=excess) + assert after is not None + + def grade(outline): + m = route_metrics( + outline.vertices, + expected, + terrain, + [], + outline.curves, + max_grade=cr["max_grade_pct"] / 100, + min_radius_m=cr["min_radius_m"], + stream_offset_m=cr["stream_offset_m"], + road_width_m=cr["road_width_m"], + bound_pct=None, + grade_smooth_m=cr["grade_smooth_m"], + ) + return m["over_limit"]["total"], m["max_grade_pct"] + + (o0, g0), (o1, g1) = grade(before), grade(after) + off = off_station_count(after) + print( + f"{case['mode']}: 측점 밖 {case['old']['off']}/{case['old']['curves']}(옛)" + f" → {off}/{len(after.curves)} · 넘은 곳 {o0} → {o1}" + f" · 최대 기울기 {g0:.1f} → {g1:.1f} %(맞추기 전 → 새)" + ) + old_total += case["old"]["off"] + new_total += off + # 다투는 쌍 없음 — 이웃 측점 밖 곡선 MC 가 20 m 안이면 한 측점을 다툼 + mcs = _off_mcs(after) + assert all(b - a >= STATION_M for a, b in zip(mcs, mcs[1:])), mcs + gap = _ls(after.vertices).hausdorff_distance(_ls(before.vertices)) + assert gap <= ROUTE_STATION_SNAP_MAX_GAP_M + 0.05 + assert after.vertices[0] == pytest.approx(before.vertices[0], abs=1e-3) + assert after.vertices[-1] == pytest.approx(before.vertices[-1], abs=1e-3) + print(f"측점 밖 곡선 합 {old_total} → {new_total}") + assert new_total * 4 <= old_total + + +def test_crowded_pair_drops_one() -> None: + """11 m 사이 두 곡선 · 이탈 한도 1 m(측점 옮김 막힘) — 옛 맞춤은 둘 다 그 자리(44.8 · 55.8).""" + points = [(0.0, 0.0), (45.0, 0.0), (54.0, 7.0), (58.0, 70.0)] + before = build_planned_polyline( + points, min_radius_m=12.0, simplify=False, curve_flags=[True] * 4 + ) + after = snap_curves_to_stations(before, 12.0, max_gap_m=1.0) + assert after is not None and len(after.curves) == 1 + assert _ls(after.vertices).hausdorff_distance(_ls(before.vertices)) <= 1.0 + 0.05