SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,170 @@
|
||||
"""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())
|
||||
Reference in new issue
Block a user