refactor(배수유역): 347-2 세부 유역 엔진 700줄 — 관경 산정(강우표 · 합리식 · 추천 구조물)을 common_util_drainage_sizing 으로 나눔
- 이름은 common_util_drainage_detail 에서도 그대로 읽힘(옛 자리 import 유지) · 내용 고침 없음 - 시험 test_b05_drainage_recommend · test_96_b08_hydro_linked · test_342_3_* 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""배수 관경 산정 — 강우량표 읽기 · 합리식 유효직경 · 추천 구조물.
|
||||
|
||||
세부 유역 엔진(`common_util_drainage_detail` · 700줄로 나눔)이 유역 제원으로 부름 ·
|
||||
이름은 그 파일에서도 그대로 읽힘.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from common_util.common_util_drainage_pipes import (
|
||||
PIPE_FACILITY_BOX,
|
||||
PIPE_FACILITY_FORD_BRIDGE,
|
||||
PIPE_FACILITY_PIPE,
|
||||
)
|
||||
from common_util.common_util_wamis_rainfall import idf_intensity
|
||||
from config.config_system import (
|
||||
DRAINAGE_BOX_THRESHOLD_MM,
|
||||
DRAINAGE_BRIDGE_THRESHOLD_MM,
|
||||
DRAINAGE_DESIGN_FLOW_FACTOR,
|
||||
DRAINAGE_FLOW_AREA_RATIO,
|
||||
DRAINAGE_MANNING_N,
|
||||
DRAINAGE_PIPE_SLOPE_DEG,
|
||||
DRAINAGE_RAINFALL_FILENAME,
|
||||
DRAINAGE_RECOMMEND_DIAMETERS_MM,
|
||||
DRAINAGE_RUNOFF_COEFFICIENT,
|
||||
DRAINAGE_TC_MIN_MINUTES,
|
||||
DRAINAGE_VELOCITY_MAX_MS,
|
||||
DRAINAGE_VELOCITY_MIN_MS,
|
||||
PIPE_DEFAULT_DIAMETER_MM,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_rainfall_idf(directory: Path) -> dict[str, float] | None:
|
||||
"""배수유역 폴더의 rainfall_table.json에서 설계빈도 IDF 적합계수를 읽는다.
|
||||
|
||||
파일은 B04 전처리(`/drainage/rainfall`)가 만든다. 없으면 None — 유효직경은
|
||||
"미정"으로 남고, 강우량표가 생기는 순간 다음 세부유역 계산부터 채워진다.
|
||||
"""
|
||||
path = Path(directory) / DRAINAGE_RAINFALL_FILENAME
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
table = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
logger.warning("강우량표를 읽지 못했습니다 (%s) — 유효직경 미산출.", path)
|
||||
return None
|
||||
idf = table.get("idf_design")
|
||||
if not isinstance(idf, dict) or not all(k in idf for k in ("a", "b", "n")):
|
||||
return None
|
||||
return idf
|
||||
|
||||
|
||||
def size_pipe(
|
||||
area_m2: float,
|
||||
relief_m: float,
|
||||
flow_length_m: float,
|
||||
idf: dict[str, float] | None,
|
||||
) -> dict[str, float] | None:
|
||||
"""유역 제원 + 설계빈도 IDF로 배수 유효직경(mm)과 산출 근거를 계산한다.
|
||||
|
||||
시행규칙 별표2 (가) 1호 방법: 100년빈도 확률강우량 + 홍수도달시간 → 합리식.
|
||||
전 과정·계수 근거: docs/raw/guidelines/2026-08-05_임도_배수설계_규정_조사.md 4·6절.
|
||||
|
||||
① 도달시간 tc = 0.0663·L^0.77·(H/L)^-0.385 [Kirpich, hr] — 하한 5분
|
||||
② 강우강도 I = General형 적합식 I(tc) [mm/hr]
|
||||
③ 합리식 Q = (1/3.6)·C·I·A[km²], 설계유량 = 2.0·Q (별표2)
|
||||
④ 유속 V = Manning(경사 10도 고정, n=0.024) → 0.8~3.0m/s 클램프
|
||||
⑤ 통수단면 70%만 유효 → D = √(4·(Q설계/V)/(0.7π))
|
||||
"""
|
||||
if idf is None or area_m2 <= 0:
|
||||
return None
|
||||
length_km = max(flow_length_m, 1.0) / 1000.0
|
||||
slope = max(relief_m, 0.1) / max(flow_length_m, 1.0)
|
||||
tc_hr = 0.0663 * (length_km**0.77) * (slope**-0.385)
|
||||
tc_min = max(DRAINAGE_TC_MIN_MINUTES, tc_hr * 60.0)
|
||||
intensity = idf_intensity(idf, tc_min)
|
||||
flow = DRAINAGE_RUNOFF_COEFFICIENT * intensity * (area_m2 / 1e6) / 3.6
|
||||
design_flow = DRAINAGE_DESIGN_FLOW_FACTOR * flow
|
||||
if design_flow <= 0:
|
||||
return None
|
||||
pipe_slope = math.tan(math.radians(DRAINAGE_PIPE_SLOPE_DEG))
|
||||
diameter = 0.5
|
||||
for _ in range(20):
|
||||
velocity = (
|
||||
(1.0 / DRAINAGE_MANNING_N) * (diameter / 4.0) ** (2.0 / 3.0) * math.sqrt(pipe_slope)
|
||||
)
|
||||
velocity = min(DRAINAGE_VELOCITY_MAX_MS, max(DRAINAGE_VELOCITY_MIN_MS, velocity))
|
||||
required_area = design_flow / velocity
|
||||
updated = math.sqrt(4.0 * required_area / (DRAINAGE_FLOW_AREA_RATIO * math.pi))
|
||||
if abs(updated - diameter) < 1e-4:
|
||||
diameter = updated
|
||||
break
|
||||
diameter = updated
|
||||
return {
|
||||
"tc_minutes": round(tc_min, 1),
|
||||
"intensity_mm_hr": round(intensity, 1),
|
||||
"design_flow_m3s": round(design_flow, 4),
|
||||
"diameter_mm": round(diameter * 1000.0, 1),
|
||||
# 물넘이·세월교는 관이 아니라 개수로라 직경 대신 이 단면적에서 출발한다.
|
||||
"required_area_m2": round(required_area, 4),
|
||||
}
|
||||
|
||||
|
||||
def recommend_structure(diameter_mm: float | None) -> tuple[str, int | None]:
|
||||
"""유효직경으로 추천 구조물과 관경을 고른다 (2026-08-17 사용자 확정: 유량 근거만).
|
||||
|
||||
지식DB 선정 트리(개거_세월시설 §5·횡단배수관_암거 §5)에서 유량 조건만 취했다 —
|
||||
계곡 횡단경사·하천 차수는 지형 계산이 필요해 화면이 "현장 확인"으로 안내한다.
|
||||
|
||||
D ≤ 1,500㎜ 배관 (레지스트리 선택지로 스냅, 하한 800㎜)
|
||||
1,500 < D ≤ 2,000 BOX암거 후보
|
||||
D > 2,000㎜ 세월교·물넘이 검토 (관 최대 규격 초과)
|
||||
|
||||
유효직경을 못 구한 유역(강우량표 없음)은 배관·관경 미정으로 둔다.
|
||||
"""
|
||||
if diameter_mm is None:
|
||||
return PIPE_FACILITY_PIPE, None
|
||||
if diameter_mm > DRAINAGE_BRIDGE_THRESHOLD_MM:
|
||||
return PIPE_FACILITY_FORD_BRIDGE, None
|
||||
if diameter_mm > DRAINAGE_BOX_THRESHOLD_MM:
|
||||
return PIPE_FACILITY_BOX, None
|
||||
# 별표2 (나) 예외 하한 800㎜ — 계산값이 더 작아도 그 아래로는 내리지 않는다.
|
||||
need = max(float(PIPE_DEFAULT_DIAMETER_MM), diameter_mm)
|
||||
size = next(
|
||||
(value for value in DRAINAGE_RECOMMEND_DIAMETERS_MM if value >= need),
|
||||
DRAINAGE_RECOMMEND_DIAMETERS_MM[-1],
|
||||
)
|
||||
return PIPE_FACILITY_PIPE, int(size)
|
||||
|
||||
|
||||
def estimate_pipe_diameter_mm(
|
||||
area_m2: float,
|
||||
relief_m: float,
|
||||
flow_length_m: float,
|
||||
idf: dict[str, float] | None = None,
|
||||
) -> float | None:
|
||||
"""유역 제원으로 배수 유효직경(mm)만 돌려주는 축약형 — 상세는 size_pipe()."""
|
||||
sizing = size_pipe(area_m2, relief_m, flow_length_m, idf)
|
||||
return sizing["diameter_mm"] if sizing else None
|
||||
Reference in New Issue
Block a user