diff --git a/common_util/common_util_drainage_detail.py b/common_util/common_util_drainage_detail.py index f369dcdc7..ca3764a94 100644 --- a/common_util/common_util_drainage_detail.py +++ b/common_util/common_util_drainage_detail.py @@ -25,7 +25,6 @@ from __future__ import annotations import json import logging -import math from dataclasses import dataclass, field from pathlib import Path from typing import Any @@ -36,10 +35,11 @@ from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import find_inflow_h from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import STAGES from B04_PreProcess.B04_PreProcess_Engine_Watershed_Flow import polygon_parts, polygonize_labels from B04_PreProcess.B04_PreProcess_Engine_Watershed_Grid import GridSpec -from common_util.common_util_drainage_pipes import ( - PIPE_FACILITY_BOX, - PIPE_FACILITY_FORD_BRIDGE, - PIPE_FACILITY_PIPE, +from common_util.common_util_drainage_sizing import ( # noqa: F401 — 옛 자리에서 읽는 곳 + estimate_pipe_diameter_mm, + load_rainfall_idf, + recommend_structure, + size_pipe, ) from common_util.common_util_route_geometry import ( RouteVertex, @@ -48,24 +48,11 @@ from common_util.common_util_route_geometry import ( is_uphill_at, ) from common_util.common_util_structure_station import snap_candidates -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_DITCH_SAMPLE_M, - DRAINAGE_FLOW_AREA_RATIO, - DRAINAGE_MANNING_N, DRAINAGE_PIPE_MAX_SPACING_M, DRAINAGE_PIPE_MIN_SPACING_M, - 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__) @@ -610,112 +597,3 @@ def _outlet_elevation(routing: RoadRouting, pipe_order: int, pipe_of_slot: np.nd elevations = routing.elevation[routing.road_cell_index[slots]] finite = elevations[np.isfinite(elevations)] return float(finite.min()) if finite.size else 0.0 - - -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 diff --git a/common_util/common_util_drainage_sizing.py b/common_util/common_util_drainage_sizing.py new file mode 100644 index 000000000..b2d121514 --- /dev/null +++ b/common_util/common_util_drainage_sizing.py @@ -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