feat(B05): 추천 갈래 다시 — 영선 · 안전 우선 · 계류 횡단 최소 · 공사비 어림 최소(PLAN 23-5a)
- 갈래 short · balance 뺌(요청 422) · gradeline · safe · crossings · cost 더함 — 각 목표로 제약 탐색한 후보를 묶음에 넣고 제 비교값으로 고름 - 탐색 목표 셋 더함: 목표 기울기 편차(영선 · 등경사선) · 위험(옆경사 35° 이상 · 계류 버퍼 안 통과 길이) · 계류 횡단 링크 · 공사비는 상대 단가 그대로 - 비교값 더함: side_slope_avg_pct · side_slope_max_pct · hazard_m · stream_crossings · stream_offset_avg_m · grade_dev_pct · cost_index - 공사비 상대 단가(절성토 1 ㎥ = 1 · 연장 1 m = 2 · 계류 횡단 1 곳 = 400)는 임시 상대값 · 브레인 임의 판정 - 요청 criteria.target_grade_pct(영선 목표 기울기 · 없으면 상한) · 응답 criteria.target_grade_pct - 시험 test_route_initial 31 · 왕복 시험은 화면이 아는 갈래만(새 넷은 23-5b 뒤) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
This commit is contained in:
@@ -25,6 +25,17 @@ SAMPLE_M = 2.0 # 측정 간격(m)
|
||||
GRADE_SMOOTH_M = 5.0
|
||||
STATION_M = 20.0 # 종단기울기 측정 간격(m) — 측점 체계 · 타당성평가 20 m 간격
|
||||
GRADE_EPS = 1e-6 # 상한과 같은 기울기는 지킨 것으로 봄
|
||||
# 안전 우선 — 옆경사가 이보다 급한 곳을 위험으로 봄(35° = 70 % · 지식DB 「35° 이상 전폭 절토」
|
||||
# `산림과임업기술(임도)/2. 임도/4. 노선측량.md`)
|
||||
SAFE_SIDE_SLOPE = math.tan(math.radians(35.0))
|
||||
# 공사비 어림 상대 단가(절성토 1 ㎥ = 1) — ⚠ 임시 상대값 · 브레인 임의 판정(PLAN 23-5 · 지식DB
|
||||
# 근거가 붙으면 바꿈). 연장 1 m = 벌개 · 노반 · 측구 등 절성토 밖 공종을 흙 2 ㎥ 로 어림(이 현장
|
||||
# 절성토가 1 m 당 3~4 ㎥ 라 두 몫이 같은 자릿수) · 계류 횡단 1 곳 = 횡단 배수 구조물(관 ·
|
||||
# 기슭막이) 400 ㎥.
|
||||
COST_EARTH_M3 = 1.0
|
||||
COST_LENGTH_M = 2.0
|
||||
COST_CROSSING = 400.0
|
||||
CROSSING_MERGE_M = 10.0 # 이 거리 안의 계류 교점은 한 횡단(갈라진 도엽 선 이음매)
|
||||
|
||||
REASON_TERRAIN = "지형상 불가능 — 복도 안 어떤 길도 {bound:.1f}% 이상 기울기를 지나야 이어짐"
|
||||
REASON_GRADE_FIT = "곡선 맞춤 뒤 지반 차 — 측점 사이 지반이 상한을 넘음"
|
||||
@@ -116,18 +127,19 @@ def route_metrics(
|
||||
grade_smooth_m: float = GRADE_SMOOTH_M,
|
||||
follow: bool = False,
|
||||
weighted: bool = False,
|
||||
target_grade: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""비교값과 제약 못 지킨 구간 목록. 종단기울기 = 측점(20 m) 사이 고른 지반 기울기.
|
||||
|
||||
`bound_pct` 는 제약을 다 지키는 길이 없을 때만 — 복도 안 어떤 길도 넘어야 하는 기울기(%).
|
||||
`follow` 는 예상노선 추적(제약 탐색 없이 예상노선 그대로) · `weighted` 는 비중 계산(제약을
|
||||
벌점으로만 따짐) — 사유가 그 뜻으로 뜬다.
|
||||
벌점으로만 따짐) — 사유가 그 뜻으로 뜬다. `target_grade` = 영선 노선 목표 기울기(비율 · 없으면
|
||||
상한) — 기울기 편차(`grade_dev_pct`)의 기준.
|
||||
"""
|
||||
grid = (terrain.y, terrain.x)
|
||||
sampler = RegularGridInterpolator(grid, terrain.z, bounds_error=False, fill_value=None)
|
||||
smooth = RegularGridInterpolator(
|
||||
grid, smoothed_ground(terrain, grade_smooth_m), bounds_error=False, fill_value=None
|
||||
)
|
||||
smoothed = smoothed_ground(terrain, grade_smooth_m)
|
||||
smooth = RegularGridInterpolator(grid, smoothed, bounds_error=False, fill_value=None)
|
||||
points, chain = _resample(vertices, SAMPLE_M)
|
||||
length = float(chain[-1])
|
||||
lookup = points[:, ::-1]
|
||||
@@ -181,8 +193,12 @@ def route_metrics(
|
||||
# 계류 이격 — 버퍼 안 구간 중 봐줄 길이를 넘는 것. 봐줌 = 건너기(계류에 닿음) 한 번의
|
||||
# 버퍼 안 길이(45° 로 가로지름) + 기점 · 종점이 버퍼 안이면 45° 로 빠져나가는 길이
|
||||
route_points = shapely.points(points)
|
||||
near = np.full(len(points), np.inf)
|
||||
crossings = 0
|
||||
if streams:
|
||||
near = shapely.distance(route_points, MultiLineString(streams))
|
||||
network = MultiLineString(streams)
|
||||
near = shapely.distance(route_points, network)
|
||||
crossings = _crossings(LineString(vertices), network)
|
||||
for first, last in _runs(near < stream_offset_m):
|
||||
ends_in = int(first == 0) + int(last == len(near) - 1)
|
||||
crosses = float(near[first : last + 1].min()) <= SAMPLE_M
|
||||
@@ -223,6 +239,17 @@ def route_metrics(
|
||||
cut = float((np.maximum(depth, 0.0).mean(axis=1) * road_width_m * weight).sum())
|
||||
fill = float((np.maximum(-depth, 0.0).mean(axis=1) * road_width_m * weight).sum())
|
||||
|
||||
# 옆경사(고른 지반 · 노선에 수직) · 위험 통과 길이(옆경사 35° 이상 또는 계류 버퍼 안)
|
||||
sy, sx = np.gradient(smoothed, _spacing(terrain.y), _spacing(terrain.x))
|
||||
side = np.abs(
|
||||
RegularGridInterpolator(grid, sx, bounds_error=False, fill_value=None)(lookup)
|
||||
* -tangent[:, 1]
|
||||
+ RegularGridInterpolator(grid, sy, bounds_error=False, fill_value=None)(lookup)
|
||||
* tangent[:, 0]
|
||||
)
|
||||
hazard = float(weight[(side >= SAFE_SIDE_SLOPE) | (near < stream_offset_m)].sum())
|
||||
target = max_grade if target_grade is None else target_grade
|
||||
deviation = np.abs(grades - target)
|
||||
offsets = shapely.distance(route_points, LineString(expected))
|
||||
counts = {
|
||||
kind: sum(1 for v in violations if v["kind"] == kind)
|
||||
@@ -245,9 +272,34 @@ def route_metrics(
|
||||
"over_limit": {**counts, "total": len(violations)},
|
||||
"offset_avg_m": round(float(offsets.mean()), 2),
|
||||
"offset_max_m": round(float(offsets.max()), 2),
|
||||
# PLAN 23-5 — 옆경사 · 위험 통과 길이(안전 우선) · 계류 횡단(계류 횡단 최소) · 계류 평균
|
||||
# 거리(계류 없으면 None) · 목표 기울기 편차(영선 노선 · 측점 기울기와 목표의 차 평균) ·
|
||||
# 공사비 어림
|
||||
"side_slope_avg_pct": round(float((side * weight).sum() / max(length, 1e-9)) * 100.0, 2),
|
||||
"side_slope_max_pct": round(float(side.max()) * 100.0, 2),
|
||||
"hazard_m": round(hazard, 1),
|
||||
"stream_crossings": crossings,
|
||||
"stream_offset_avg_m": round(float(near.mean()), 2) if streams else None,
|
||||
"grade_dev_pct": round(
|
||||
float((deviation * spans[keep]).sum() / spans[keep].sum()) * 100.0
|
||||
if grades.size
|
||||
else 0.0,
|
||||
2,
|
||||
),
|
||||
"cost_index": round(
|
||||
COST_EARTH_M3 * (cut + fill) + COST_LENGTH_M * length + COST_CROSSING * crossings
|
||||
),
|
||||
"violations": violations,
|
||||
}
|
||||
|
||||
|
||||
def _crossings(route: LineString, network: MultiLineString) -> int:
|
||||
"""노선이 계류를 건너는 횟수 — 교점을 노선 따라 세고 `CROSSING_MERGE_M` 안은 한 번."""
|
||||
hit = route.intersection(network)
|
||||
points = [geom for geom in getattr(hit, "geoms", [hit]) if not geom.is_empty]
|
||||
at = sorted(route.project(point.centroid) for point in points)
|
||||
return sum(1 for i, value in enumerate(at) if i == 0 or value - at[i - 1] > CROSSING_MERGE_M)
|
||||
|
||||
|
||||
def _spacing(coords: np.ndarray) -> float:
|
||||
return float((coords[-1] - coords[0]) / (len(coords) - 1)) if len(coords) > 1 else 1.0
|
||||
|
||||
Reference in New Issue
Block a user