457 lines
15 KiB
Python
457 lines
15 KiB
Python
"""管网分区模块。"""
|
|
|
|
import math
|
|
|
|
import matplotlib.pyplot as plt
|
|
import networkx as nx
|
|
import networkx as networkx
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pymetis
|
|
from scipy.sparse import coo_matrix, csr_matrix
|
|
from scipy.sparse.csgraph import connected_components
|
|
|
|
|
|
def _to_metis_edge_weight(edge_weight):
|
|
weight = float(edge_weight)
|
|
if not math.isfinite(weight):
|
|
raise ValueError(f"Invalid non-finite METIS edge weight: {edge_weight}")
|
|
# pymetis expects integer edge weights.
|
|
return max(1, int(round(weight)))
|
|
|
|
|
|
def _dedupe_preserve_order(items):
|
|
seen = set()
|
|
output = []
|
|
for item in items:
|
|
if item in seen:
|
|
continue
|
|
seen.add(item)
|
|
output.append(item)
|
|
return output
|
|
|
|
|
|
def pick_center_pipe(node_x, node_y, candidate_pipe, pipe_start_node, pipe_end_node):
|
|
candidate_pipe_list = list(candidate_pipe)
|
|
start_nodes = pipe_start_node[candidate_pipe_list]
|
|
end_nodes = pipe_end_node[candidate_pipe_list]
|
|
|
|
x_vals = (node_x[start_nodes].to_numpy() + node_x[end_nodes].to_numpy()) / 2.0
|
|
y_vals = (node_y[start_nodes].to_numpy() + node_y[end_nodes].to_numpy()) / 2.0
|
|
mean_x = float(np.mean(x_vals))
|
|
mean_y = float(np.mean(y_vals))
|
|
distance = np.abs(x_vals - mean_x) + np.abs(y_vals - mean_y)
|
|
center_idx = int(np.argmin(distance))
|
|
return candidate_pipe_list[center_idx]
|
|
|
|
|
|
def pick_max_diameter_pipe(candidate_pipe, pipe_diameter):
|
|
candidate_pipe_list = list(candidate_pipe)
|
|
diameters = pd.to_numeric(
|
|
pipe_diameter.reindex(candidate_pipe_list), errors="coerce"
|
|
).dropna()
|
|
if len(diameters) != len(candidate_pipe_list):
|
|
missing = sorted(set(candidate_pipe_list) - set(diameters.index))
|
|
preview = ", ".join(map(str, missing[:10]))
|
|
raise ValueError(f"Missing or invalid diameter for pipes: {preview}")
|
|
|
|
max_diameter = float(diameters.max())
|
|
max_diameter_pipes = sorted(
|
|
[pipe for pipe, diameter in diameters.items() if float(diameter) == max_diameter],
|
|
key=str,
|
|
)
|
|
return max_diameter_pipes[0]
|
|
|
|
|
|
def pick_dual_center_pipes(
|
|
node_x, node_y, candidate_pipe, pipe_start_node, pipe_end_node, pipe_diameter
|
|
):
|
|
geometric_center = pick_center_pipe(
|
|
node_x, node_y, candidate_pipe, pipe_start_node, pipe_end_node
|
|
)
|
|
diameter_center = pick_max_diameter_pipe(candidate_pipe, pipe_diameter)
|
|
return _dedupe_preserve_order([geometric_center, diameter_center])
|
|
|
|
|
|
def find_new_center_pipe(
|
|
node_x,
|
|
node_y,
|
|
candidate_pipe,
|
|
pipe_start_node,
|
|
pipe_end_node,
|
|
pipe_diameter,
|
|
record_center,
|
|
):
|
|
new_candidate_pipe = sorted(set(candidate_pipe) - set(record_center))
|
|
if new_candidate_pipe == []:
|
|
new_candidate_pipe = candidate_pipe
|
|
center_t = pick_center_pipe(
|
|
node_x,
|
|
node_y,
|
|
new_candidate_pipe,
|
|
pipe_start_node,
|
|
pipe_end_node,
|
|
)
|
|
return center_t
|
|
|
|
|
|
def cal_area_node_linked_pipe(nodeset, node_pipe_dic):
|
|
pipeset = []
|
|
for temp_node in nodeset:
|
|
pipeset.extend(node_pipe_dic[temp_node])
|
|
return pipeset
|
|
|
|
|
|
def metis_grouping_pipe_weight(
|
|
G0,
|
|
wn,
|
|
all_node_iter,
|
|
candidate_pipe_input,
|
|
group_num,
|
|
node_x,
|
|
node_y,
|
|
pipe_start_node_all,
|
|
pipe_end_node_all,
|
|
node_pipe_dic,
|
|
all_node_series,
|
|
couple_node_length,
|
|
pipe_diameter,
|
|
):
|
|
all_node_iter_series_new = all_node_series[all_node_iter]
|
|
all_node_iter_series_new = all_node_iter_series_new.sort_values(ascending=True)
|
|
all_node_iter_new = list(all_node_iter_series_new.index)
|
|
|
|
G1 = G0.subgraph(all_node_iter_new)
|
|
delimiter = " "
|
|
adjacency_list = []
|
|
node_dict = {}
|
|
c_new = 0
|
|
for each_node in all_node_iter_new:
|
|
node_dict[each_node] = c_new
|
|
c_new = c_new + 1
|
|
correspond_dic = {}
|
|
count_node = 0
|
|
w = []
|
|
for node_name in all_node_iter_new:
|
|
neighbors = G1[node_name]
|
|
w_temp = []
|
|
n_t = [node_dict[node_name]]
|
|
for neighbor_name in sorted(neighbors.keys()):
|
|
edge_data = neighbors[neighbor_name]
|
|
edge_key = f"{node_name},{neighbor_name}"
|
|
reverse_edge_key = f"{neighbor_name},{node_name}"
|
|
if edge_key in couple_node_length:
|
|
edge_weight = couple_node_length[edge_key]
|
|
elif reverse_edge_key in couple_node_length:
|
|
edge_weight = couple_node_length[reverse_edge_key]
|
|
elif edge_data.get("weight") is not None:
|
|
edge_weight = float(edge_data["weight"])
|
|
else:
|
|
# Ignore graph edges that are outside candidate pipes and have no usable
|
|
# partition weight (e.g. some non-pipe links in mixed network graphs).
|
|
continue
|
|
w_temp.append(_to_metis_edge_weight(edge_weight))
|
|
n_t.append(node_dict[neighbor_name])
|
|
w.append(w_temp)
|
|
correspond_dic[n_t[0]] = count_node
|
|
count_node = count_node + 1
|
|
# del n_t[0]
|
|
adjacency_list.append(n_t)
|
|
adjacency_list_new = [[] * 1 for i in range(len(adjacency_list))]
|
|
w_new = [[] * 1 for i in range(len(adjacency_list))]
|
|
for i in range(len(adjacency_list)):
|
|
adjacency_list_new[int(adjacency_list[i][0])] = adjacency_list[i]
|
|
w_new[int(adjacency_list[i][0])] = w[i]
|
|
|
|
for i in range(len(adjacency_list)):
|
|
del adjacency_list_new[i][0]
|
|
xadj = [0]
|
|
w_f = []
|
|
final_adjacency_list = []
|
|
for i in range(len(adjacency_list_new)):
|
|
final_adjacency_list = final_adjacency_list + adjacency_list_new[i]
|
|
xadj.append(len(final_adjacency_list))
|
|
w_f = w_f + w_new[i]
|
|
|
|
# (edgecuts, parts) = pymetis.part_graph(nparts=group_num, adjacency=adjacency_list_new)
|
|
metis_options = pymetis.Options()
|
|
metis_options.seed = 42
|
|
(edgecuts, parts) = pymetis.part_graph(
|
|
nparts=group_num,
|
|
adjncy=final_adjacency_list,
|
|
xadj=xadj,
|
|
eweights=w_f,
|
|
options=metis_options,
|
|
)
|
|
# (edgecuts, parts) = pymetis.part_graph(nparts=group_num, adjacency=adjacency_list_new)
|
|
candidate_group_list = [[] * 1 for i in range(group_num)]
|
|
for i in range(len(all_node_iter_new)):
|
|
candidate_group_list[parts[i]].append(all_node_iter_new[i])
|
|
|
|
"""parts_new = np.zeros(len(candidate_node_input), dtype=int)
|
|
for i in range(len(candidate_group_list)):
|
|
temp_group = candidate_group_list[i]
|
|
for each_node in temp_group:
|
|
parts_new[node_dict[each_node]] = i
|
|
parts_new = list(parts_new)"""
|
|
|
|
new_center = []
|
|
new_group = []
|
|
new_center_candidates = []
|
|
new_all_node = []
|
|
candidate_pipe_set = set(candidate_pipe_input)
|
|
all_grouped_pipe = []
|
|
for i in range(group_num):
|
|
# 构建子图
|
|
G_sub = G0.subgraph(candidate_group_list[i])
|
|
|
|
# 计算联通子图
|
|
sub_graphs = networkx.connected_components(G_sub)
|
|
if networkx.number_connected_components(G_sub) == 1:
|
|
# 求交集
|
|
nodeset = G_sub.nodes()
|
|
pipeset_set = set(cal_area_node_linked_pipe(nodeset, node_pipe_dic))
|
|
candidate_pipe = sorted(pipeset_set.intersection(candidate_pipe_set))
|
|
|
|
# 判断集合是否保留
|
|
if len(candidate_pipe) > 0:
|
|
# 保留 计算中心
|
|
center_t = pick_center_pipe(
|
|
node_x,
|
|
node_y,
|
|
candidate_pipe,
|
|
pipe_start_node_all,
|
|
pipe_end_node_all,
|
|
)
|
|
center_candidates_t = pick_dual_center_pipes(
|
|
node_x,
|
|
node_y,
|
|
candidate_pipe,
|
|
pipe_start_node_all,
|
|
pipe_end_node_all,
|
|
pipe_diameter,
|
|
)
|
|
|
|
# 更新
|
|
new_center.append(center_t)
|
|
new_center_candidates.append(center_candidates_t)
|
|
new_group.append(candidate_pipe)
|
|
new_all_node.append(nodeset)
|
|
all_grouped_pipe = all_grouped_pipe + candidate_pipe
|
|
else:
|
|
for c in sorted(sub_graphs, key=lambda c: min(c)):
|
|
G_temp = G0.subgraph(c)
|
|
nodeset = G_temp.nodes()
|
|
pipeset = cal_area_node_linked_pipe(nodeset, node_pipe_dic)
|
|
pipeset_set = set(pipeset)
|
|
|
|
# 求交集
|
|
candidate_pipe = sorted(pipeset_set.intersection(candidate_pipe_set))
|
|
# print(len(candidate_node))
|
|
# 判断集合是否保留
|
|
if len(candidate_pipe) > 0:
|
|
# 保留 计算中心
|
|
center_t = pick_center_pipe(
|
|
node_x,
|
|
node_y,
|
|
candidate_pipe,
|
|
pipe_start_node_all,
|
|
pipe_end_node_all,
|
|
)
|
|
center_candidates_t = pick_dual_center_pipes(
|
|
node_x,
|
|
node_y,
|
|
candidate_pipe,
|
|
pipe_start_node_all,
|
|
pipe_end_node_all,
|
|
pipe_diameter,
|
|
)
|
|
# 更新
|
|
new_center.append(center_t)
|
|
new_center_candidates.append(center_candidates_t)
|
|
new_group.append(candidate_pipe)
|
|
new_all_node.append(nodeset)
|
|
all_grouped_pipe = all_grouped_pipe + candidate_pipe
|
|
record_center = []
|
|
c_g = 0
|
|
for each_group in new_group:
|
|
if len(each_group) < 3:
|
|
record_center.append(new_center[c_g])
|
|
c_g += 1
|
|
c_g = 0
|
|
for each_group in new_group:
|
|
if len(each_group) >= 3:
|
|
if new_center[c_g] in record_center:
|
|
new_center[c_g] = find_new_center_pipe(
|
|
node_x,
|
|
node_y,
|
|
each_group,
|
|
pipe_start_node_all,
|
|
pipe_end_node_all,
|
|
pipe_diameter,
|
|
record_center,
|
|
)
|
|
new_center_candidates[c_g] = _dedupe_preserve_order(
|
|
[new_center[c_g]] + list(new_center_candidates[c_g])
|
|
)
|
|
record_center.append(new_center[c_g])
|
|
c_g += 1
|
|
|
|
# visualize_metis_partition(
|
|
# G0, new_center, new_group,
|
|
# node_x, node_y,
|
|
# pipe_start_node_all, pipe_end_node_all
|
|
# )
|
|
return new_center, new_group, new_all_node, new_center_candidates
|
|
|
|
|
|
def visualize_metis_partition(
|
|
G,
|
|
center_pipes,
|
|
pipe_groups,
|
|
node_x,
|
|
node_y,
|
|
pipe_start_node_all,
|
|
pipe_end_node_all,
|
|
title: str | None = None,
|
|
block: bool = True,
|
|
pause_seconds: float | None = None,
|
|
):
|
|
"""
|
|
可视化METIS分区结果(单图模式)
|
|
参数:
|
|
G: 原始管网图(nx.Graph)
|
|
center_pipes: 中心管道列表(list)
|
|
pipe_groups: 分组管道列表(list of lists)
|
|
node_x: 节点X坐标字典(dict)
|
|
node_y: 节点Y坐标字典(dict)
|
|
pipe_start_node_all: 管道起点字典(dict)
|
|
pipe_end_node_all: 管道终点字典(dict)
|
|
"""
|
|
fig = plt.figure("metis_partition_convergence", figsize=(22.51, 12.48))
|
|
fig.clf()
|
|
ax = fig.add_subplot(111)
|
|
if not block:
|
|
plt.ion()
|
|
|
|
# 生成颜色映射(自动扩展颜色数量)
|
|
colors = plt.cm.tab20(np.linspace(0, 1, len(pipe_groups)))
|
|
|
|
# --- 绘制背景管网(灰色半透明) ---
|
|
for edge in G.edges():
|
|
start_node, end_node = edge
|
|
ax.plot(
|
|
[node_x[start_node], node_x[end_node]],
|
|
[node_y[start_node], node_y[end_node]],
|
|
color="lightgray",
|
|
linewidth=0.5,
|
|
alpha=0.3,
|
|
zorder=1, # 确保背景在底层
|
|
)
|
|
|
|
# --- 绘制各分区管道(彩色)---
|
|
legend_handles = [] # 用于图例的句柄
|
|
for i, (group, center) in enumerate(zip(pipe_groups, center_pipes)):
|
|
color = colors[i % len(colors)] # 循环使用颜色
|
|
|
|
# 绘制分组管道
|
|
for pipe in group:
|
|
start = pipe_start_node_all[pipe]
|
|
end = pipe_end_node_all[pipe]
|
|
line = ax.plot(
|
|
[node_x[start], node_x[end]],
|
|
[node_y[start], node_y[end]],
|
|
color=color,
|
|
linewidth=2.5,
|
|
alpha=0.8,
|
|
zorder=2,
|
|
)
|
|
# 只为每个分组的第一个管道添加图例句柄
|
|
if pipe == group[0]:
|
|
legend_handles.append(line[0])
|
|
|
|
# 高亮中心管道(红色虚线)
|
|
if center in pipe_start_node_all and center in pipe_end_node_all:
|
|
start = pipe_start_node_all[center]
|
|
end = pipe_end_node_all[center]
|
|
ax.plot(
|
|
[node_x[start], node_x[end]],
|
|
[node_y[start], node_y[end]],
|
|
color="red",
|
|
linewidth=4,
|
|
linestyle="--",
|
|
dash_capstyle="round",
|
|
zorder=3, # 确保中心管道在最顶层
|
|
)
|
|
|
|
# --- 添加图例和标注 ---
|
|
# 分组图例
|
|
if legend_handles:
|
|
group_labels = [f"Group {i + 1}" for i in range(len(pipe_groups))]
|
|
ax.legend(
|
|
legend_handles,
|
|
group_labels,
|
|
loc="upper right",
|
|
title="Partitions",
|
|
fontsize=8,
|
|
title_fontsize=10,
|
|
)
|
|
|
|
# 中心管道标注(可选)
|
|
for i, center in enumerate(center_pipes):
|
|
if center in pipe_start_node_all:
|
|
x = (
|
|
node_x[pipe_start_node_all[center]] + node_x[pipe_end_node_all[center]]
|
|
) / 2
|
|
y = (
|
|
node_y[pipe_start_node_all[center]] + node_y[pipe_end_node_all[center]]
|
|
) / 2
|
|
ax.text(
|
|
x,
|
|
y,
|
|
f"C{i + 1}",
|
|
color="red",
|
|
fontsize=10,
|
|
ha="center",
|
|
va="center",
|
|
bbox=dict(facecolor="white", alpha=0.8, edgecolor="none"),
|
|
)
|
|
|
|
# --- 图形美化 ---
|
|
ax.set_title(title or "Water Network Partitioning Overview", fontsize=14, pad=20)
|
|
ax.set_xlabel("X Coordinate", fontsize=10)
|
|
ax.set_ylabel("Y Coordinate", fontsize=10)
|
|
ax.grid(True, alpha=0.2, linestyle=":")
|
|
fig.tight_layout()
|
|
|
|
# 显示图形并强制刷新,避免迭代显示滞后一轮。
|
|
plt.show(block=block)
|
|
if not block:
|
|
fig.canvas.draw_idle()
|
|
fig.canvas.flush_events()
|
|
pause_value = 0.001 if pause_seconds is None else max(0.0, float(pause_seconds))
|
|
plt.pause(max(0.001, pause_value))
|
|
elif pause_seconds is not None:
|
|
plt.pause(max(0.0, float(pause_seconds)))
|
|
return fig
|
|
|
|
|
|
def generate_adjlist_with_all_edges(G, delimiter):
|
|
for s, nbrs in G.adjacency():
|
|
line = str(s) + delimiter
|
|
for t, data in nbrs.items():
|
|
line += str(t) + delimiter
|
|
yield line[: -len(delimiter)]
|
|
|
|
|
|
def cal_group_num(candidate_node_input, cal_group_num):
|
|
candidate_node_num = len(candidate_node_input)
|
|
if candidate_node_num > 100:
|
|
group_num_input = cal_group_num # 30
|
|
else:
|
|
group_num_input = 10
|
|
|
|
return group_num_input
|
|
|
|
|