"""배수 관경 산정 — 강우량표 읽기 · 합리식 유효직경 · 추천 구조물. 세부 유역 엔진(`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