- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
123 lines
4.8 KiB
Python
123 lines
4.8 KiB
Python
"""Step 1 — EPANET 모델 로드 및 단순 그래프 변환."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import networkx as nx
|
|
import pandas as pd
|
|
import wntr
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class NetworkBundle:
|
|
wn: "wntr.network.WaterNetworkModel"
|
|
graph: nx.Graph
|
|
isolated_nodes: list[str]
|
|
connectivity_warning: Optional[str] = None
|
|
|
|
|
|
def load_network(inp_path: str | Path, connections_csv: Optional[str | Path] = None) -> NetworkBundle:
|
|
wn = wntr.network.WaterNetworkModel(str(inp_path))
|
|
|
|
G = _build_simple_graph(wn)
|
|
_inject_node_attributes(wn, G)
|
|
_load_connections(G, connections_csv)
|
|
|
|
isolated = [n for n in G.nodes if G.degree(n) == 0]
|
|
if isolated:
|
|
logger.warning("%d개 노드가 어떤 관로에도 연결되어 있지 않습니다: %s", len(isolated), isolated[:10])
|
|
|
|
connectivity_warning = None
|
|
if G.number_of_nodes() > 0 and not nx.is_connected(G):
|
|
n_components = nx.number_connected_components(G)
|
|
connectivity_warning = f"네트워크가 {n_components}개의 연결되지 않은 구성요소로 나뉘어 있습니다"
|
|
logger.warning(connectivity_warning)
|
|
|
|
return NetworkBundle(wn=wn, graph=G, isolated_nodes=isolated, connectivity_warning=connectivity_warning)
|
|
|
|
|
|
def _build_simple_graph(wn: "wntr.network.WaterNetworkModel") -> nx.Graph:
|
|
multi = wn.to_graph() # MultiDiGraph, 노드 좌표 등 기본 속성 포함
|
|
|
|
G = nx.Graph()
|
|
G.add_nodes_from(multi.nodes)
|
|
|
|
grouped: dict[tuple[str, str], list[str]] = {}
|
|
for u, v, key in multi.edges(keys=True):
|
|
pair = (u, v) if u <= v else (v, u)
|
|
grouped.setdefault(pair, []).append(key)
|
|
|
|
for (u, v), link_names in grouped.items():
|
|
lengths, diameters, roughness = [], [], []
|
|
for name in link_names:
|
|
link = wn.get_link(name)
|
|
lengths.append(getattr(link, "length", 0.0) or 0.0)
|
|
diameters.append(getattr(link, "diameter", 0.0) or 0.0)
|
|
roughness.append(getattr(link, "roughness", 0.0) or 0.0)
|
|
|
|
if len(link_names) == 1:
|
|
length, diameter, rough = lengths[0], diameters[0], roughness[0]
|
|
else:
|
|
# 병렬관 등가화: 단면적(D^2) 합산 기준 등가관경, 연장은 최단 경로 기준 최소값.
|
|
# 실제 수리해석은 원본 wn(EPANET 엔진)으로 수행하므로 여기서는 분할 가중치용 근사치.
|
|
length = min(lengths) if lengths else 0.0
|
|
diameter = (sum(d**2 for d in diameters)) ** 0.5 if diameters else 0.0
|
|
rough = max(roughness) if roughness else 0.0
|
|
|
|
G.add_edge(u, v, length=length, diameter=diameter, roughness=rough, pipe_ids=list(link_names))
|
|
|
|
return G
|
|
|
|
|
|
def _inject_node_attributes(wn: "wntr.network.WaterNetworkModel", G: nx.Graph) -> None:
|
|
for node_name in G.nodes:
|
|
node = wn.get_node(node_name)
|
|
|
|
elevation = getattr(node, "elevation", None)
|
|
if elevation is None:
|
|
elevation = getattr(node, "base_head", 0.0) # Reservoir
|
|
|
|
base_demand = 0.0
|
|
for demand in getattr(node, "demand_timeseries_list", []) or []:
|
|
base_demand += demand.base_value
|
|
|
|
G.nodes[node_name]["elevation"] = float(elevation or 0.0)
|
|
G.nodes[node_name]["base_demand"] = float(base_demand)
|
|
G.nodes[node_name]["coordinates"] = node.coordinates
|
|
G.nodes[node_name]["node_type"] = type(node).__name__
|
|
G.nodes[node_name]["connections"] = None
|
|
G.nodes[node_name]["is_large_consumer_csv"] = False
|
|
|
|
|
|
def _load_connections(G: nx.Graph, connections_csv: Optional[str | Path]) -> None:
|
|
if not connections_csv:
|
|
logger.warning(
|
|
"connections.csv가 제공되지 않았습니다. 급수전수 기반 제약"
|
|
"(constraints.connections_per_block)은 이번 실행에서 평가되지 않습니다."
|
|
)
|
|
return
|
|
|
|
df = pd.read_csv(connections_csv, dtype={"node_id": str})
|
|
missing = {"node_id", "connections"} - set(df.columns)
|
|
if missing:
|
|
raise ValueError(f"connections.csv에 필수 컬럼이 없습니다: {sorted(missing)}")
|
|
|
|
unknown = 0
|
|
for _, row in df.iterrows():
|
|
node_id = str(row["node_id"])
|
|
if node_id not in G.nodes:
|
|
unknown += 1
|
|
continue
|
|
G.nodes[node_id]["connections"] = float(row["connections"])
|
|
if "is_large_consumer" in df.columns and not pd.isna(row["is_large_consumer"]):
|
|
G.nodes[node_id]["is_large_consumer_csv"] = bool(row["is_large_consumer"])
|
|
|
|
if unknown:
|
|
logger.warning("connections.csv의 %d개 행이 네트워크 노드와 일치하지 않아 무시되었습니다", unknown)
|