PLAN 0-2 항목 9 / 0-3. 노선 편집 [확인]이 재확정 체인을 타는데 그 체인이 solve_route 로 BP·CP·EP 사이를 격자에서 다시 풀었음. 사용자가 노드를 조금만 비틀어도 탐색 제약에 걸려 체인이 통째로 멈췄음(보조 창 로그: 세그먼트 1 (BP -> CP1) 경로 탐색 실패 — 종단경사 한계 26% · 최소곡선반지름 12m · 회피지역 제약). - B05_Profile_Engine_AsPlanned.solve_as_planned 신설 — 제어점 목록이 곧 노선이고 표고만 지표면 격자에서 뜸. 반환 꼴은 솔버와 같아 아래 단계가 그대로 이어짐. 종단기울기·곡선반지름 위반은 **세되 막지 않음**(사용자 확정: 자동 보정·차단 없이 경고만). - run_route_design 에 algorithm=as_planned 갈래 추가. 스키마 검증에도 허용. - 재확정 체인이 그 갈래를 씀 — 자동탐색은 이제 초기 업로드에서만 돎. 솔버와 지표 계산이 겹치지만 합치지 않음 — 솔버는 0-3 으로 접히는 코드라 리팩터링해 두 곳을 얽을 값어치가 없음. 시험 409 통과·17 건너뜀, main import 스모크 통과. 실화면 [확인] 재검증은 보조 창이 자기 프로젝트에서 진행. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
170 lines
6.7 KiB
Python
170 lines
6.7 KiB
Python
"""사용자가 고친 계획노선을 **그대로 쓰는** 길 — 다시 풀지 않는다.
|
|
|
|
왜 필요한가(2026-09-06 실측) — 노선 편집 [확인]이 재확정 체인을 타는데, 그 체인이
|
|
`solve_route` 로 **BP·CP·EP 제어점 사이를 격자에서 다시 풀었다**. 사용자가 노드를 조금만
|
|
비틀어도 탐색 제약에 걸려 체인이 통째로 멈췄다:
|
|
|
|
세그먼트 1 (BP → CP1) 경로 탐색 실패: 종단경사 한계(26%)·최소곡선반지름(12m)·
|
|
회피지역 제약으로 통과 경로가 없습니다.
|
|
|
|
그 결과 배수유역·관은 새 노선으로 가고 종횡단만 옛 노선에 남아 배수관 측점이 9 → 0 이 됐다.
|
|
PLAN 0-3 에서 **노선 자동탐색은 접기로** 했고(사용자 확정), 계획노선은 사용자가 직접 고친다.
|
|
여기서는 그 노선을 **그대로 받아** 표고만 지표면에서 떠서 결과 꼴을 맞춘다.
|
|
|
|
**위반은 세되 막지 않는다** — 종단기울기·곡선반지름을 재어 개수로 알리되 경로를 바꾸지
|
|
않는다(사용자 확정: 자동 보정·차단 없이 경고만). 막는 순간 「내가 그린 선이 안 들어간다」가 된다.
|
|
|
|
솔버(`B05_Profile_Engine_Solver`)와 지표 계산이 겹치지만 **합치지 않았다** — 솔버는 0-3 으로
|
|
접히는 코드라 그쪽을 리팩터링해 두 곳을 얽을 값어치가 없다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import math
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_MODELS_SUBDIR = "B04_PreProcess/models"
|
|
|
|
|
|
def _elevation_lookup(
|
|
project_root: Path, filter_key: str, method: str, smooth: bool
|
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
"""지표면 격자(x, y, z) — 노선 정점의 표고를 뜨는 데 쓴다."""
|
|
from B05_Profile.B05_Profile_Engine_Solver import _load_dtm_grid, _sample_surface_on_grid
|
|
|
|
models_dir = Path(project_root) / _MODELS_SUBDIR
|
|
x_coords, y_coords, dtm_z, _valid = _load_dtm_grid(models_dir, filter_key, smooth)
|
|
surface_z = _sample_surface_on_grid(
|
|
models_dir, filter_key, method, smooth, x_coords, y_coords, dtm_z
|
|
)
|
|
return x_coords, y_coords, surface_z
|
|
|
|
|
|
def _z_at(
|
|
x_coords: np.ndarray, y_coords: np.ndarray, z_grid: np.ndarray, x: float, y: float
|
|
) -> float:
|
|
"""격자에서 가장 가까운 칸의 표고. 격자 밖이면 가장자리 값."""
|
|
col = int(np.clip(np.searchsorted(x_coords, x), 0, len(x_coords) - 1))
|
|
row = int(np.clip(np.searchsorted(y_coords, y), 0, len(y_coords) - 1))
|
|
value = float(z_grid[row, col])
|
|
return value if math.isfinite(value) else 0.0
|
|
|
|
|
|
def solve_as_planned(
|
|
project_root: Path,
|
|
filter_key: str,
|
|
smooth: bool,
|
|
vertices: list[tuple[float, float]],
|
|
options: dict[str, Any],
|
|
method: str = "dtm",
|
|
) -> dict[str, Any]:
|
|
"""계획노선 정점을 그대로 노선으로 삼는다. 반환 꼴은 솔버와 같다."""
|
|
if len(vertices) < 2:
|
|
raise ValueError("계획노선 정점이 2개 미만입니다.")
|
|
x_coords, y_coords, z_grid = _elevation_lookup(project_root, filter_key, method, smooth)
|
|
polyline = [[float(x), float(y), _z_at(x_coords, y_coords, z_grid, x, y)] for x, y in vertices]
|
|
|
|
max_uphill = float(options.get("max_uphill_grade") or 0.26)
|
|
max_downhill = float(options.get("max_downhill_grade") or 0.26)
|
|
min_radius = float(options.get("min_curve_radius_m") or 12.0)
|
|
|
|
count = len(polyline)
|
|
chainage_m = [0.0] * count
|
|
length_m = 0.0
|
|
grade_sums = 0.0
|
|
max_grade = 0.0
|
|
max_up = 0.0
|
|
max_down = 0.0
|
|
slope_violations = 0
|
|
for index in range(count - 1):
|
|
x1, y1, z1 = polyline[index]
|
|
x2, y2, z2 = polyline[index + 1]
|
|
horizontal = math.hypot(x2 - x1, y2 - y1)
|
|
chainage_m[index + 1] = chainage_m[index] + horizontal
|
|
if horizontal <= 0.01:
|
|
continue
|
|
dz = z2 - z1
|
|
slope = abs(dz) / horizontal
|
|
length_m += horizontal
|
|
grade_sums += slope * horizontal
|
|
max_grade = max(max_grade, slope)
|
|
if dz > 0:
|
|
max_up = max(max_up, slope)
|
|
else:
|
|
max_down = max(max_down, slope)
|
|
if slope > (max_uphill if dz > 0 else max_downhill):
|
|
slope_violations += 1
|
|
avg_grade = (grade_sums / length_m) if length_m > 0 else 0.0
|
|
|
|
# 곡선반지름 — 세 점을 지나는 원으로 재고, 하한을 밑도는 자리를 **세기만** 한다.
|
|
from B05_Profile.B05_Profile_Engine_Geometry import circumradius_2d
|
|
|
|
curve_violations = 0
|
|
min_radius_actual = float("inf")
|
|
for index in range(1, count - 1):
|
|
radius = circumradius_2d(polyline[index - 1], polyline[index], polyline[index + 1])
|
|
min_radius_actual = min(min_radius_actual, radius)
|
|
if radius < min_radius:
|
|
curve_violations += 1
|
|
|
|
total_length = chainage_m[-1] if chainage_m else 0.0
|
|
segments = [
|
|
{
|
|
"index": 0,
|
|
"from": "BP",
|
|
"to": "EP",
|
|
"point_start": 0,
|
|
"point_end": count - 1,
|
|
"chainage_start_m": 0.0,
|
|
"chainage_end_m": round(total_length, 2),
|
|
"length_m": round(total_length, 2),
|
|
"max_grade_pct": round(max_grade * 100, 2),
|
|
}
|
|
]
|
|
logger.info(
|
|
"계획노선 그대로 사용: 정점 %d · 연장 %.1fm · 경사위반 %d · 곡선위반 %d",
|
|
count,
|
|
total_length,
|
|
slope_violations,
|
|
curve_violations,
|
|
)
|
|
return {
|
|
"polyline": polyline,
|
|
"chainage_m": [round(value, 3) for value in chainage_m],
|
|
"segments": segments,
|
|
# 사용자가 그린 선이므로 제어점 도달 검사는 뜻이 없다 — 통과로 둔다.
|
|
"required_point_checks": [],
|
|
"required_points_ok": True,
|
|
"avoid_intrusions": [],
|
|
"forbidden_intrusions": [],
|
|
"curve_warning_segments": [],
|
|
"avoid_retry_performed": False,
|
|
"conditions_snapshot": {
|
|
"filter": filter_key,
|
|
"method": method,
|
|
"smooth": smooth,
|
|
"source": "as_planned",
|
|
"min_curve_radius_m": round(min_radius, 2),
|
|
},
|
|
"metrics": {
|
|
"length_m": round(length_m, 2),
|
|
"avg_grade_pct": round(avg_grade * 100, 2),
|
|
"max_grade_pct": round(max_grade * 100, 2),
|
|
"max_uphill_pct": round(max_up * 100, 2),
|
|
"max_downhill_pct": round(max_down * 100, 2),
|
|
"slope_violations": slope_violations,
|
|
"search_max_grade_pct": round(max(max_uphill, max_downhill) * 100, 2),
|
|
"curve_violations": curve_violations,
|
|
"min_curve_radius_m": round(min_radius_actual, 2)
|
|
if math.isfinite(min_radius_actual)
|
|
else None,
|
|
"min_curve_radius_limit_m": round(min_radius, 2),
|
|
},
|
|
}
|