- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
171 lines
6.5 KiB
Python
171 lines
6.5 KiB
Python
"""CLI: python -m blocksys run --inp network.inp --config config.yaml [--k K] --out ./result
|
|
|
|
MVP(Step 1~6) 범위: 로드 -> 기준 수리해석 -> 가중 그래프 -> 분할 -> 경계밸브/유량계 ->
|
|
분할안 수리적 검증. balance/refine, score.py, report.html, valve_segment_analysis는
|
|
아직 구현되지 않았다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import logging
|
|
import math
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import wntr
|
|
|
|
from .boundary import BoundaryLink, apply_blocks_to_model, assign_valve_or_meter, find_boundary_links
|
|
from .config import Config
|
|
from .graph import apply_hard_constraints, build_weighted_graph
|
|
from .io import load_network
|
|
from .partition import partition
|
|
from .validate import ValidationResult, baseline_simulation, hydraulic_validation
|
|
|
|
logger = logging.getLogger("blocksys")
|
|
|
|
DISCLAIMER = (
|
|
"본 결과는 블록분할 검토 초안이며, 최종 블록 경계는 업무편람 구축절차 ⑤ "
|
|
"블록고립 현장조사를 거쳐 확정한다."
|
|
)
|
|
|
|
|
|
def build_arg_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(prog="blocksys")
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
run = sub.add_parser("run", help="블록 분할 대안 생성 및 수리적 검증 (MVP: Step 1~6)")
|
|
run.add_argument("--inp", required=True, type=Path, help="EPANET .inp 파일 경로")
|
|
run.add_argument("--config", type=Path, default=None, help="config.yaml 경로 (생략 시 기본값)")
|
|
run.add_argument("--connections", type=Path, default=None, help="connections.csv 경로 (선택)")
|
|
run.add_argument("--k", type=int, default=None, help="단일 대안만 생성. 미지정 시 k_min~k_max 스윕")
|
|
run.add_argument("--out", type=Path, default=Path("./result"), help="출력 디렉터리")
|
|
|
|
return parser
|
|
|
|
|
|
def _ensure_utf8_stdio() -> None:
|
|
# Windows 콘솔 기본 코드페이지(cp949 등)에서 한글 로그가 깨지는 것을 방지.
|
|
for stream in (sys.stdout, sys.stderr):
|
|
try:
|
|
stream.reconfigure(encoding="utf-8")
|
|
except (AttributeError, ValueError):
|
|
pass
|
|
|
|
|
|
def run_command(args: argparse.Namespace) -> int:
|
|
_ensure_utf8_stdio()
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
|
|
|
cfg = Config.from_yaml(args.config) if args.config else Config()
|
|
args.out.mkdir(parents=True, exist_ok=True)
|
|
|
|
logger.info("네트워크 로드: %s", args.inp)
|
|
bundle = load_network(args.inp, args.connections)
|
|
|
|
logger.info("기준 수리해석(baseline) 실행 중...")
|
|
baseline = baseline_simulation(bundle.wn, cfg)
|
|
|
|
Gw = build_weighted_graph(bundle.graph, baseline, cfg)
|
|
Gw = apply_hard_constraints(Gw, cfg)
|
|
|
|
k_values = [args.k] if args.k is not None else list(range(cfg.partition.k_min, cfg.partition.k_max + 1))
|
|
|
|
summary_rows = []
|
|
for k in k_values:
|
|
logger.info("k=%d 대안 생성 중...", k)
|
|
try:
|
|
labels = partition(Gw, k, cfg)
|
|
except ValueError as exc:
|
|
logger.warning("k=%d 분할 실패: %s", k, exc)
|
|
continue
|
|
|
|
boundary = find_boundary_links(Gw, labels)
|
|
boundary = assign_valve_or_meter(boundary, baseline, inlets_per_block=cfg.boundary.inlets_per_block)
|
|
wn_blocked = apply_blocks_to_model(bundle.wn, boundary)
|
|
validation = hydraulic_validation(wn_blocked, baseline, cfg)
|
|
|
|
_write_boundary_csv(args.out / f"boundary_valves_k{k}.csv", boundary)
|
|
wntr.network.io.write_inpfile(wn_blocked, str(args.out / f"alt_{k}.inp"))
|
|
|
|
summary_rows.append(_summarize(k, Gw, boundary, validation))
|
|
logger.info(
|
|
"k=%d: valve=%d meter=%d status=%s",
|
|
k,
|
|
sum(1 for b in boundary if b.action == "close"),
|
|
sum(1 for b in boundary if b.action == "meter"),
|
|
validation.status,
|
|
)
|
|
|
|
_write_summary_csv(args.out / "validation.csv", summary_rows)
|
|
|
|
logger.info("완료. 결과 디렉터리: %s", args.out)
|
|
logger.info(DISCLAIMER)
|
|
return 0
|
|
|
|
|
|
def _summarize(k: int, Gw, boundary: list[BoundaryLink], validation: ValidationResult) -> dict:
|
|
boundary_length = sum(
|
|
Gw[b.from_node][b.to_node].get("length", 0.0) for b in boundary if Gw.has_edge(b.from_node, b.to_node)
|
|
)
|
|
return {
|
|
"k": k,
|
|
"n_valves": sum(1 for b in boundary if b.action == "close"),
|
|
"n_meters": sum(1 for b in boundary if b.action == "meter"),
|
|
"boundary_pipe_length_m": round(boundary_length, 1),
|
|
"min_pressure_m": _fmt(validation.min_pressure_m),
|
|
"pressure_violations": len(validation.pressure_low_violations) + len(validation.pressure_high_violations),
|
|
"isolated_nodes": len(validation.isolated_nodes),
|
|
"max_water_age_h": _fmt(validation.max_water_age_h),
|
|
"status": validation.status,
|
|
"rejection_reason": validation.rejection_reason or "",
|
|
}
|
|
|
|
|
|
def _fmt(value: float) -> str:
|
|
return "" if value is None or math.isnan(value) else round(value, 2)
|
|
|
|
|
|
def _write_boundary_csv(path: Path, boundary: list[BoundaryLink]) -> None:
|
|
fieldnames = ["pipe_id", "from_node", "to_node", "block_a", "block_b", "action", "baseline_flow", "diameter"]
|
|
with path.open("w", newline="", encoding="utf-8-sig") as f:
|
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
for b in boundary:
|
|
writer.writerow(
|
|
{
|
|
"pipe_id": b.pipe_id,
|
|
"from_node": b.from_node,
|
|
"to_node": b.to_node,
|
|
"block_a": b.block_a,
|
|
"block_b": b.block_b,
|
|
"action": b.action,
|
|
"baseline_flow": b.baseline_flow,
|
|
"diameter": b.diameter,
|
|
}
|
|
)
|
|
|
|
|
|
def _write_summary_csv(path: Path, rows: list[dict]) -> None:
|
|
if not rows:
|
|
logger.warning("유효한 대안이 없어 validation.csv를 생성하지 않았습니다")
|
|
return
|
|
with path.open("w", newline="", encoding="utf-8-sig") as f:
|
|
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
|
|
writer.writeheader()
|
|
writer.writerows(rows)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = build_arg_parser()
|
|
args = parser.parse_args(argv)
|
|
if args.command == "run":
|
|
return run_command(args)
|
|
parser.print_help()
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|