"""管网分区模块。""" 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