fix(B05): 가까운 제어점 구간은 격자 탐색 없이 직결해 원청 계획노선을 보존한다

계획노선 CSV는 설계 원청이 지정한 기준선이라 프로그램이 바꾸면 안 되는데,
격자 Dijkstra가 제어점 사이에 셀 중심점을 끼워 넣어 평면이 틀어졌다.
제어점은 원좌표로 되박히지만 끼어든 점은 되박히지 않기 때문이다.

원인은 셀 크기와 정점 간격의 기하 관계다. LAS 프로젝트는 비용면 셀이
2.188m(원본 DTM 해상도, 다운샘플 배수 1)라 정점 간격(평균 2.61m·최대
3.25m)이 전부 8이웃 한 걸음에 들어가 중간점이 안 생겼다. 도엽 등고선
프로젝트는 DTM이 1.0m라 배수 2로 셀이 2.0m가 되고, 20쌍이 두 걸음으로
벌어져 22개 점이 끼어들었다(최대 이탈 2.08m, 평면 +4.72m, 곡선 경고 증가).
솔버에 LAS 전용 보존 코드는 없다 — 셀 2.0m에 LAS CSV를 스냅해도 두 걸음
쌍이 16개 생긴다.

제어점 쌍의 평면 거리가 ROUTE_DIRECT_LINK_CELL_FACTOR×셀 이하면 탐색을
건너뛰고 두 칸을 그대로 잇는다. 조밀 기준선(정점 전체가 제어점)은 평면이
원본과 완전히 같아지고, 간격이 문턱을 넘는 구간만 종전대로 탐색한다.
경사·곡선 제약은 이 구간에서 경고로만 남는다.

검증(프로젝트 0aca395f 실데이터, 원본 미변경 사본에서 재탐색):
- 셀 2.0m: 정점 156→135, 끼어든 점 22→0, 이탈 2.076→0.000m,
  평면 354.84→350.11m(CSV와 동일), 곡선 경고 9→8
- 셀 3.0m: 111→135 정점(같은 칸에 합쳐지던 24개도 살아남음), 이탈 0.000m
- LAS 프로젝트 c1bb453f: 재탐색 결과 xy 차 0.000000m — 결과 불변
- pytest 14 passed (직결·같은 칸 미병합·먼 구간 탐색 유지 3건 추가)
This commit is contained in:
2026-08-30 19:38:24 +09:00
parent 932d85348e
commit 72ead694b1
2 changed files with 35 additions and 19 deletions
+32 -19
View File
@@ -23,6 +23,7 @@ from config.config_system import (
FOREST_ROAD_MAX_GRADE, FOREST_ROAD_MAX_GRADE,
FOREST_ROAD_MIN_CURVE_R_M, FOREST_ROAD_MIN_CURVE_R_M,
ROUTE_DEFAULT_GRADE_CLASS, ROUTE_DEFAULT_GRADE_CLASS,
ROUTE_DIRECT_LINK_CELL_FACTOR,
ROUTE_GRID_RES_M, ROUTE_GRID_RES_M,
ROUTE_MAX_COST_CELLS, ROUTE_MAX_COST_CELLS,
ROUTE_MAX_GRADE, ROUTE_MAX_GRADE,
@@ -390,31 +391,43 @@ def solve_optimal_route(
full_path_grid: list[tuple[int, int]] = [] full_path_grid: list[tuple[int, int]] = []
segment_bounds: list[dict[str, Any]] = [] segment_bounds: list[dict[str, Any]] = []
direct_link_max_m = ROUTE_DIRECT_LINK_CELL_FACTOR * target_res
for i in range(len(sequence) - 1): for i in range(len(sequence) - 1):
pt_start = sequence[i] pt_start = sequence[i]
pt_end = sequence[i + 1] pt_end = sequence[i + 1]
r_s, c_s, _ = get_grid_indices(pt_start) r_s, c_s, _ = get_grid_indices(pt_start)
r_e, c_e, _ = get_grid_indices(pt_end) r_e, c_e, _ = get_grid_indices(pt_end)
segment = single_segment_dijkstra( # 제어점 쌍이 문턱보다 가까우면 격자 탐색 없이 직결한다. 두 끝은 어차피
r_s, # 원좌표로 되박히므로 이 구간 평면은 원청 계획노선 그대로 보존된다.
c_s, # 같은 칸에 스냅돼도 두 항목을 유지해 정점이 합쳐져 사라지지 않게 한다.
r_e, # 경사·곡선반경 제약은 이 구간에선 경고(curve_warning_segments)로만 남는다.
c_e, if (
x_coords_sub, math.hypot(pt_end["x"] - pt_start["x"], pt_end["y"] - pt_start["y"])
y_coords_sub, <= direct_link_max_m
z_grid_sub, ):
valid_mask_sub, segment = [(r_s, c_s), (r_e, c_e)]
dz_dx, else:
dz_dy, segment = single_segment_dijkstra(
ap_list, r_s,
weights, c_s,
max_grade, r_e,
target_res, c_e,
min_curve_radius_m, x_coords_sub,
max_uphill_grade, y_coords_sub,
max_downhill_grade, z_grid_sub,
) valid_mask_sub,
dz_dx,
dz_dy,
ap_list,
weights,
max_grade,
target_res,
min_curve_radius_m,
max_uphill_grade,
max_downhill_grade,
)
if not segment: if not segment:
fp_note = "·금지구역(FP)" if fp_list else "" fp_note = "·금지구역(FP)" if fp_list else ""
raise ValueError( raise ValueError(
+3
View File
@@ -261,6 +261,9 @@ ROUTE_AVOID_DEFAULT_RADIUS_M = float(os.getenv("ROUTE_AVOID_DEFAULT_RADIUS_M", "
ROUTE_DEFAULT_GRADE_CLASS = os.getenv("ROUTE_DEFAULT_GRADE_CLASS", "trunk") ROUTE_DEFAULT_GRADE_CLASS = os.getenv("ROUTE_DEFAULT_GRADE_CLASS", "trunk")
ROUTE_MAX_COST_CELLS = int(os.getenv("ROUTE_MAX_COST_CELLS", "4000000")) ROUTE_MAX_COST_CELLS = int(os.getenv("ROUTE_MAX_COST_CELLS", "4000000"))
ROUTE_REQUIRED_POINT_TOLERANCE_M = float(os.getenv("ROUTE_REQUIRED_POINT_TOLERANCE_M", "1.0")) ROUTE_REQUIRED_POINT_TOLERANCE_M = float(os.getenv("ROUTE_REQUIRED_POINT_TOLERANCE_M", "1.0"))
# 제어점 쌍이 이 배수×비용면 셀보다 가까우면 격자 탐색 없이 직결한다 — 원청 계획노선
# 보존 (2026-08-30 사용자 확정. 조밀 기준선은 격자 중간점이 못 끼어들어 평면 불변).
ROUTE_DIRECT_LINK_CELL_FACTOR = float(os.getenv("ROUTE_DIRECT_LINK_CELL_FACTOR", "2.0"))
# 임도 종류 — 현행 규칙(별표2)의 3종. `branch`(지선)는 규칙에서 폐지됐으나 기존 # 임도 종류 — 현행 규칙(별표2)의 3종. `branch`(지선)는 규칙에서 폐지됐으나 기존
# 저장분이 남아 있어 값으로는 계속 받는다(화면 선택지에서는 뺀다, 2026-08-19). # 저장분이 남아 있어 값으로는 계속 받는다(화면 선택지에서는 뺀다, 2026-08-19).
ROUTE_GRADE_CLASSES = ("trunk", "fire", "work", "branch") ROUTE_GRADE_CLASSES = ("trunk", "fire", "work", "branch")