Files
Aislo/B08_Quantity/B08_Quantity_Hydro_Site.py
T
eomsangdonandClaude Opus 5.5 311c43d83a feat(B08): 수리 표 측점마다 B05 배수유역 연결값 · 표준값 B05 배수 설정 하나로(96-1 · 96-3)
- 04_detailed_basins + pipe_points → linked(A · L · H · 관경 · 박스 내공 · 월류 폭) · 참고 b05(Qd · T)
- 측점 짝 carry_indices(±0.05 m) · 하상경사 · 관 종류 · n 은 안 이음
- 유출계수 · 할증 · 재현기간 · 최소 도달시간 = config DRAINAGE_* import
- 시험 test_96_b08_hydro_linked(연결값 · Qd ≈ B05 설계유량)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
2026-10-08 13:45:32 +09:00

138 lines
5.7 KiB
Python

"""B08 4탭 홍수량 계산서 — 공사지 지역 · 확률강우량 · 저장 파일 (PLAN 69-5 · 69-6).
읽기만: 전처리 강우량표(`B04_PreProcess/drainage/rainfall_table.json` · 관측소 확률강우량)
· 행정구역(`B04_PreProcess/processed/행정구역_*_bounds.geojson` · WGS84) · 프로젝트 등록 지역(DB)
· B05 세부유역(`B04_PreProcess/drainage/04_detailed_basins.geojson` · B05 [저장] 때 씀 · 96-1).
배수유역 계산 코드는 부르지 않음 — 이미 저장된 파일만 읽음.
쓰기: B08 자기 파일 `B08_Quantity/quantities/hydro_inputs.json` 하나([저장] 때만).
"""
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any
from shapely.geometry import Point, shape
from config.config_system_design import DRAINAGE_CACHE_DIRNAME, DRAINAGE_DETAIL_FILENAME
RAINFALL_FILE = Path("B04_PreProcess") / DRAINAGE_CACHE_DIRNAME / "rainfall_table.json"
BASINS_FILE = Path("B04_PreProcess") / DRAINAGE_CACHE_DIRNAME / DRAINAGE_DETAIL_FILENAME
PROCESSED_DIR = Path("B04_PreProcess") / "processed"
EMD_FILE = "행정구역_읍면동_bounds.geojson"
SGG_FILE = "행정구역_시군구_bounds.geojson"
SAVED_FILE = Path("B08_Quantity") / "quantities" / "hydro_inputs.json"
#: 강우량표가 없을 때 재현기간 드롭다운 — 확률강우량 자료 표의 재현기간
FALLBACK_PERIODS = (2, 3, 5, 10, 20, 30, 50, 70, 80, 100, 200, 300, 500)
DAY_MINUTES = 1440
def _read_json(path: Path) -> dict[str, Any] | None:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
return data if isinstance(data, dict) else None
def _features(path: Path) -> list[dict[str, Any]]:
data = _read_json(path) or {}
return [f for f in data.get("features") or [] if isinstance(f, dict)]
def _feature_at(path: Path, lon: float, lat: float) -> dict[str, Any] | None:
"""점이 드는 행정구역 속성 — 경계 파일은 WGS84."""
point = Point(lon, lat)
for feature in _features(path):
try:
if shape(feature["geometry"]).covers(point):
return feature.get("properties") or {}
except (KeyError, TypeError, ValueError):
continue
return None
def _region(root: Path, rain: dict[str, Any] | None, db_region: str | None) -> dict[str, str]:
"""공사지 지역 — 전처리 행정구역(노선 대표점이 드는 곳) > 시군구 하나뿐 > 프로젝트 등록 지역."""
lat, lon = (rain or {}).get("lat"), (rain or {}).get("lon")
if isinstance(lat, (int, float)) and isinstance(lon, (int, float)):
emd = _feature_at(root / PROCESSED_DIR / EMD_FILE, lon, lat)
sgg = _feature_at(root / PROCESSED_DIR / SGG_FILE, lon, lat)
name = (sgg or {}).get("sig_kor_nm") or ""
full = (emd or {}).get("full_nm") or (sgg or {}).get("full_nm") or ""
if name or full:
return {"name": name or full.split()[-1], "full": full, "source": "전처리 행정구역"}
sggs = _features(root / PROCESSED_DIR / SGG_FILE)
if len(sggs) == 1:
props = sggs[0].get("properties") or {}
return {
"name": props.get("sig_kor_nm") or "",
"full": props.get("full_nm") or "",
"source": "전처리 행정구역",
}
if db_region:
return {"name": db_region, "full": db_region, "source": "프로젝트 등록"}
return {"name": "", "full": "", "source": ""}
def _r24_table(rain: dict[str, Any] | None) -> dict[int, float]:
"""재현기간(년) → 24시간 확률강우량(mm)."""
table: dict[int, float] = {}
for row in (rain or {}).get("values") or []:
try:
if int(row["duration_min"]) == DAY_MINUTES:
table[int(row["return_period_yr"])] = float(row["depth_mm"])
except (KeyError, TypeError, ValueError):
continue
return table
def site_info(root: Path, db_region: str | None) -> dict[str, Any]:
"""화면 머리 — 지역 · 관측소 · 재현기간 목록 · 재현기간별 R24(없으면 빈 표)."""
rain = _read_json(root / RAINFALL_FILE)
r24 = _r24_table(rain)
station = (rain or {}).get("station") or {}
return {
"region": _region(root, rain, db_region),
"station": station.get("name") or "",
"periods": sorted(r24) or list(FALLBACK_PERIODS),
"r24": {str(k): v for k, v in sorted(r24.items())},
}
def site_r24(root: Path, return_period: float) -> float | None:
"""계산 때 서버가 정본으로 쓰는 R24 — 자료가 있으면 그 값 · 없으면 None(화면 입력)."""
return _r24_table(_read_json(root / RAINFALL_FILE)).get(int(return_period))
def read_basins(root: Path) -> list[dict[str, Any]]:
"""B05 세부유역 속성(kind detail_basin) — 파일이 없으면 빈 목록(연결값은 시설 옵션만)."""
basins = []
for feature in _features(root / BASINS_FILE):
props = feature.get("properties") or {}
if props.get("kind") == "detail_basin" and isinstance(
props.get("chainage_m"), (int, float)
):
basins.append(props)
return basins
def read_saved(root: Path) -> dict[str, dict[str, Any]]:
data = _read_json(root / SAVED_FILE) or {}
stations = data.get("stations")
return stations if isinstance(stations, dict) else {}
def write_saved(root: Path, stations: dict[str, dict[str, Any]]) -> None:
"""원자적 쓰기 — 임시 파일에 쓴 뒤 바꿔 끼움."""
path = root / SAVED_FILE
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".part")
payload = {"version": 1, "stations": stations}
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
os.replace(tmp, path)