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
Whitespace-only changes.
@@ -0,0 +1,37 @@
|
||||
"""공통 픽스처. wntr 동봉 Net3.inp를 개발용 데이터로 사용 (§7)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import wntr
|
||||
|
||||
from blocksys.config import Config
|
||||
from blocksys.graph import apply_hard_constraints, build_weighted_graph
|
||||
from blocksys.io import load_network
|
||||
from blocksys.validate import baseline_simulation
|
||||
|
||||
NET3_PATH = wntr.library.model_library.get_filepath("Net3")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def cfg() -> Config:
|
||||
c = Config()
|
||||
c.hydraulics.duration_hours = 6 # 테스트 속도를 위해 24 -> 6시간으로 단축
|
||||
c.hydraulics.timestep_minutes = 60
|
||||
return c
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def bundle():
|
||||
return load_network(NET3_PATH)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def baseline(bundle, cfg):
|
||||
return baseline_simulation(bundle.wn, cfg)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def weighted_graph(bundle, baseline, cfg):
|
||||
Gw = build_weighted_graph(bundle.graph, baseline, cfg)
|
||||
return apply_hard_constraints(Gw, cfg)
|
||||
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from blocksys.boundary import apply_blocks_to_model, assign_valve_or_meter, find_boundary_links
|
||||
from blocksys.partition import partition
|
||||
|
||||
|
||||
def test_boundary_matches_differing_labels(weighted_graph, cfg):
|
||||
labels = partition(weighted_graph, 6, cfg)
|
||||
boundary = find_boundary_links(weighted_graph, labels)
|
||||
|
||||
expected_pairs = {
|
||||
(u, v)
|
||||
for u, v in weighted_graph.edges
|
||||
if u in labels and v in labels and labels[u] != labels[v]
|
||||
}
|
||||
actual_pairs = {(b.from_node, b.to_node) for b in boundary}
|
||||
assert actual_pairs == expected_pairs
|
||||
|
||||
|
||||
def test_each_boundary_link_has_a_real_pipe_id(weighted_graph, bundle, cfg):
|
||||
labels = partition(weighted_graph, 6, cfg)
|
||||
boundary = find_boundary_links(weighted_graph, labels)
|
||||
for b in boundary:
|
||||
assert b.pipe_id in bundle.wn.link_name_list
|
||||
|
||||
|
||||
def test_assign_valve_or_meter_gives_every_block_an_inlet(weighted_graph, baseline, cfg):
|
||||
labels = partition(weighted_graph, 6, cfg)
|
||||
boundary = find_boundary_links(weighted_graph, labels)
|
||||
boundary = assign_valve_or_meter(boundary, baseline, inlets_per_block=cfg.boundary.inlets_per_block)
|
||||
|
||||
assert {b.action for b in boundary} <= {"close", "meter"}
|
||||
|
||||
blocks_touched = {b.block_a for b in boundary} | {b.block_b for b in boundary}
|
||||
for block in blocks_touched:
|
||||
links = [b for b in boundary if b.block_a == block or b.block_b == block]
|
||||
assert any(b.action == "meter" for b in links)
|
||||
|
||||
|
||||
def test_apply_blocks_to_model_only_closes_marked_links(weighted_graph, bundle, baseline, cfg):
|
||||
labels = partition(weighted_graph, 6, cfg)
|
||||
boundary = find_boundary_links(weighted_graph, labels)
|
||||
boundary = assign_valve_or_meter(boundary, baseline, inlets_per_block=cfg.boundary.inlets_per_block)
|
||||
|
||||
already_closed = {
|
||||
name for name, link in bundle.wn.links() if str(link.initial_status) == "Closed"
|
||||
}
|
||||
wn_blocked = apply_blocks_to_model(bundle.wn, boundary)
|
||||
|
||||
newly_closed_expected = {b.pipe_id for b in boundary if b.action == "close"}
|
||||
closed_actual = {
|
||||
name for name, link in wn_blocked.links() if str(link.initial_status) == "Closed"
|
||||
}
|
||||
assert closed_actual == already_closed | newly_closed_expected
|
||||
# 원본 모델은 변경되지 않아야 한다
|
||||
assert all(
|
||||
str(bundle.wn.get_link(pid).initial_status) != "Closed"
|
||||
for pid in newly_closed_expected - already_closed
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def test_node_preservation(bundle, weighted_graph):
|
||||
assert set(weighted_graph.nodes) == set(bundle.graph.nodes)
|
||||
|
||||
|
||||
def test_edge_preservation(bundle, weighted_graph):
|
||||
assert weighted_graph.number_of_edges() == bundle.graph.number_of_edges()
|
||||
|
||||
|
||||
def test_weight_range(weighted_graph):
|
||||
weights = [d["w"] for _, _, d in weighted_graph.edges(data=True)]
|
||||
assert weights, "가중치가 부여된 엣지가 없습니다"
|
||||
assert all(0.0 < w <= 1.0 for w in weights)
|
||||
|
||||
|
||||
def test_large_consumer_edges_suppressed(bundle, weighted_graph, baseline, cfg):
|
||||
from blocksys.graph import build_weighted_graph
|
||||
|
||||
large_nodes = [n for n, d in weighted_graph.nodes(data=True) if d.get("is_large_consumer")]
|
||||
if not large_nodes:
|
||||
return # Net3 기본 설정(5 L/s)에는 대수용가가 없을 수 있음
|
||||
|
||||
plain = build_weighted_graph(bundle.graph, baseline, cfg)
|
||||
# 대수용가 인접 엣지는 제약 반영 전보다 가중치가 낮아야 한다(isolate 기본 정책)
|
||||
for n in large_nodes:
|
||||
for u, v, d in weighted_graph.edges(n, data=True):
|
||||
assert d["w"] <= plain[u][v]["w"]
|
||||
|
||||
|
||||
def test_reproducibility(bundle, baseline, cfg):
|
||||
from blocksys.graph import apply_hard_constraints, build_weighted_graph
|
||||
|
||||
g1 = apply_hard_constraints(build_weighted_graph(bundle.graph, baseline, cfg), cfg)
|
||||
g2 = apply_hard_constraints(build_weighted_graph(bundle.graph, baseline, cfg), cfg)
|
||||
|
||||
for u, v, d in g1.edges(data=True):
|
||||
assert g2[u][v]["w"] == d["w"]
|
||||
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import networkx as nx
|
||||
import pytest
|
||||
|
||||
from blocksys.partition import partition
|
||||
|
||||
K_VALUES = [4, 6, 8]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("k", K_VALUES)
|
||||
def test_node_preservation(weighted_graph, cfg, k):
|
||||
labels = partition(weighted_graph, k, cfg)
|
||||
assert set(labels.keys()) == set(weighted_graph.nodes)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("k", K_VALUES)
|
||||
def test_block_connectivity(weighted_graph, cfg, k):
|
||||
labels = partition(weighted_graph, k, cfg)
|
||||
blocks: dict[int, list] = {}
|
||||
for n, label in labels.items():
|
||||
blocks.setdefault(label, []).append(n)
|
||||
|
||||
for label, nodes in blocks.items():
|
||||
sub = weighted_graph.subgraph(nodes)
|
||||
assert nx.is_connected(sub), f"block {label} is not a single connected component"
|
||||
|
||||
|
||||
def test_reproducibility(weighted_graph, cfg):
|
||||
labels1 = partition(weighted_graph, 6, cfg)
|
||||
labels2 = partition(weighted_graph, 6, cfg)
|
||||
assert labels1 == labels2
|
||||
|
||||
|
||||
def test_k_too_large_raises(weighted_graph, cfg):
|
||||
with pytest.raises(ValueError):
|
||||
partition(weighted_graph, weighted_graph.number_of_nodes() + 1, cfg)
|
||||
|
||||
|
||||
def test_louvain_method(weighted_graph, cfg):
|
||||
cfg.partition.method = "louvain"
|
||||
try:
|
||||
labels = partition(weighted_graph, 6, cfg)
|
||||
assert set(labels.keys()) == set(weighted_graph.nodes)
|
||||
finally:
|
||||
cfg.partition.method = "spectral"
|
||||
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from blocksys.boundary import BoundaryLink, apply_blocks_to_model
|
||||
from blocksys.validate import hydraulic_validation
|
||||
|
||||
|
||||
def test_baseline_simulation_has_all_nodes(bundle, baseline):
|
||||
assert set(baseline.node_pressure_mean.keys()) == set(bundle.wn.node_name_list)
|
||||
assert set(baseline.link_flow_mean_abs.keys()) == set(bundle.wn.link_name_list)
|
||||
|
||||
|
||||
def test_baseline_pressures_are_finite(baseline):
|
||||
assert all(math.isfinite(v) for v in baseline.node_pressure_mean.values())
|
||||
|
||||
|
||||
def test_hydraulic_validation_runs_on_unmodified_network(bundle, baseline, cfg):
|
||||
result = hydraulic_validation(bundle.wn, baseline, cfg)
|
||||
assert result.status in ("ok", "violated")
|
||||
assert not result.isolated_nodes
|
||||
|
||||
|
||||
def test_isolation_is_detected_and_rejected(bundle, baseline, cfg):
|
||||
graph = bundle.graph
|
||||
source_names = set(bundle.wn.reservoir_name_list) | set(bundle.wn.tank_name_list)
|
||||
|
||||
leaf = next(n for n in graph.nodes if graph.degree(n) == 1 and n not in source_names)
|
||||
_, neighbor, edge_data = next(iter(graph.edges(leaf, data=True)))
|
||||
pipe_id = edge_data["pipe_ids"][0]
|
||||
|
||||
boundary = [
|
||||
BoundaryLink(
|
||||
pipe_id=pipe_id,
|
||||
from_node=leaf,
|
||||
to_node=neighbor,
|
||||
block_a=0,
|
||||
block_b=1,
|
||||
action="close",
|
||||
baseline_flow=0.0,
|
||||
diameter=0.0,
|
||||
)
|
||||
]
|
||||
wn_blocked = apply_blocks_to_model(bundle.wn, boundary)
|
||||
|
||||
result = hydraulic_validation(wn_blocked, baseline, cfg)
|
||||
assert leaf in result.isolated_nodes
|
||||
assert result.status == "rejected"
|
||||
assert result.rejection_reason
|
||||
|
||||
|
||||
def test_allow_isolated_nodes_still_simulates(bundle, baseline, cfg):
|
||||
graph = bundle.graph
|
||||
source_names = set(bundle.wn.reservoir_name_list) | set(bundle.wn.tank_name_list)
|
||||
leaf = next(n for n in graph.nodes if graph.degree(n) == 1 and n not in source_names)
|
||||
_, neighbor, edge_data = next(iter(graph.edges(leaf, data=True)))
|
||||
pipe_id = edge_data["pipe_ids"][0]
|
||||
|
||||
boundary = [
|
||||
BoundaryLink(
|
||||
pipe_id=pipe_id, from_node=leaf, to_node=neighbor, block_a=0, block_b=1,
|
||||
action="close", baseline_flow=0.0, diameter=0.0,
|
||||
)
|
||||
]
|
||||
wn_blocked = apply_blocks_to_model(bundle.wn, boundary)
|
||||
|
||||
cfg.hydraulics.allow_isolated_nodes = True
|
||||
try:
|
||||
result = hydraulic_validation(wn_blocked, baseline, cfg)
|
||||
assert leaf in result.isolated_nodes
|
||||
assert result.status == "violated"
|
||||
finally:
|
||||
cfg.hydraulics.allow_isolated_nodes = False
|
||||
Reference in new issue
Block a user