Files
bjkim1 21ebb16fb7 SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음)
- README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리
- 패치내역을 docs/로 이동, _worklog.md 추가
- 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
2026-10-30 11:00:46 +09:00

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())