refactor(backend)!: separate algorithm and data layers

Reorganize algorithm packages by business responsibility, move orchestration into services, and keep database access behind pooled repositories.

Harden analysis API validation, remove unsafe legacy simulation endpoints, and add regression and architecture boundary coverage.

BREAKING CHANGE: legacy algorithm module paths and obsolete simulation endpoints are removed.
This commit is contained in:
2026-09-04 17:30:55 +08:00
parent 9b095c7439
commit 5966d039de
91 changed files with 1418 additions and 4020 deletions
@@ -0,0 +1,3 @@
from .pipeline import run_burst_location
__all__ = ["run_burst_location"]
@@ -0,0 +1,771 @@
"""爆管定位主模块。"""
import copy
import math
import os
import sys
from datetime import datetime
from time import perf_counter
import networkx as nx
import numpy as np
import pandas as pd
from .leak_signature import cal_signature_pipe_multi_pf
from .topology_partitioning import (
cal_group_num,
metis_grouping_pipe_weight,
visualize_metis_partition,
)
from .similarity_metrics import (
adjust_ratio,
cal_similarity_all_multi_new_sq_improve_double_lzr,
decode_mode,
extra_judge,
update_similarity,
)
def _ensure_signatures_for_centers(
wn,
wn_inp_path,
center_list, # 本轮要用到的中心(list[str])
pressure_leak_all,
flow_leak_all, # 全量缓存(可为空 DF
timestep_list, # 你现有的时序列表
pressure_monitor,
flow_monitor, # 用来推断传感器列名
leak_mag,
n_workers=1,
):
"""
只为缺失的中心补算 SLF(调用你现有的 cal_signature_pipe_multi_pf),
并把补算结果并回缓存。返回:
pressure_leak_subset, flow_leak_subset, pressure_leak_all_new, flow_leak_all_new
其中 subset 只包含 center_list 的行(顺序与 center_list 保持一致)。
"""
center_list = _dedupe_preserve_order(center_list)
# 1) 推断传感器列名(与现有数据保持一致)
sensor_name_all = list(pressure_monitor.columns)
sensor_f_name_all = (
list(flow_monitor.columns)
if (flow_monitor is not None and hasattr(flow_monitor, "columns"))
else []
)
# 2) 取出缓存里已经有的中心(考虑 MultiIndex 的第 0 层为 pipe
def _existing_pipes(df):
if df is None or len(df) == 0:
return set()
idx = df.index
if isinstance(idx, pd.MultiIndex):
return set(idx.get_level_values(0))
else:
return set(idx)
exist_p = _existing_pipes(pressure_leak_all)
need = [p for p in center_list if p not in exist_p]
# 3) 若有缺失中心,仅为这些中心补算一次
if len(need) > 0:
p_new, _ = cal_signature_pipe_multi_pf(
wn,
leak_mag,
need,
timestep_list,
sensor_name_all,
n_workers=n_workers,
wn_inp_path=wn_inp_path,
)
# 初始化空缓存时,做一次“同构化”
if pressure_leak_all is None or len(pressure_leak_all) == 0:
pressure_leak_all = p_new
else:
pressure_leak_all = pd.concat([pressure_leak_all, p_new], axis=0)
# if (flow_leak_all is None or len(flow_leak_all) == 0) and f_new is not None:
# flow_leak_all = f_new
# elif f_new is not None:
# flow_leak_all = pd.concat([flow_leak_all, f_new], axis=0)
# 去重(如果既有缓存里不小心有重复中心)
if isinstance(pressure_leak_all.index, pd.MultiIndex):
pressure_leak_all = pressure_leak_all[
~pressure_leak_all.index.duplicated(keep="last")
]
if flow_leak_all is not None and len(flow_leak_all) > 0:
flow_leak_all = flow_leak_all[
~flow_leak_all.index.duplicated(keep="last")
]
else:
pressure_leak_all = pressure_leak_all[
~pressure_leak_all.index.duplicated(keep="last")
]
if flow_leak_all is not None and len(flow_leak_all) > 0:
flow_leak_all = flow_leak_all[
~flow_leak_all.index.duplicated(keep="last")
]
# 4) 从更新后的缓存里,取出这轮需要的中心子集(顺序与 center_list 一致)
if isinstance(pressure_leak_all.index, pd.MultiIndex):
pressure_subset = pressure_leak_all.loc[center_list]
flow_subset = (
flow_leak_all.loc[center_list]
if (flow_leak_all is not None and len(flow_leak_all) > 0)
else None
)
else:
pressure_subset = pressure_leak_all.loc[center_list, :]
flow_subset = (
flow_leak_all.loc[center_list, :]
if (flow_leak_all is not None and len(flow_leak_all) > 0)
else None
)
return pressure_subset, flow_subset, pressure_leak_all, flow_leak_all
def area_output_num_ki_improve(
candidate_center,
candidate_group,
similarity,
new_all_node,
top_group_ratio,
top_pipe_num_max,
top_pipe_num_min,
cut_ratio,
):
final_area = []
final_center = []
all_node_iter = []
if similarity.index.is_unique == False:
total_center_num = len(set(similarity.index))
else:
total_center_num = len(similarity.index)
next_group_num = min(
total_center_num, math.ceil(total_center_num / cut_ratio * top_group_ratio)
)
for i in range(next_group_num):
top_center = similarity.index[i]
top_center_index = find_list_repeat(candidate_center, top_center)
for j in range(len(top_center_index)):
final_area = final_area + candidate_group[top_center_index[j]]
all_node_iter = all_node_iter + list(new_all_node[top_center_index[j]])
final_center.append(top_center)
final_area = sorted(set(final_area))
if len(final_area) > top_pipe_num_max:
if_end = 0
elif len(final_area) > top_pipe_num_min:
if_end = 1
elif total_center_num == next_group_num:
if_end = 1
else:
if_end = 1
for i in np.arange(next_group_num, total_center_num, 1):
before_list = copy.deepcopy(final_area)
top_center = similarity.index[i]
top_center_index = candidate_center.index(top_center)
temp_group = final_area + candidate_group[top_center_index]
temp_area = sorted(set(temp_group))
if len(temp_area) < top_pipe_num_min:
final_center.append(top_center)
all_node_iter = all_node_iter + list(new_all_node[top_center_index])
final_area = temp_area
elif len(temp_area) < top_pipe_num_max:
final_center.append(top_center)
all_node_iter = all_node_iter + list(new_all_node[top_center_index])
final_area = temp_area
break
else:
a = len(temp_area) - top_pipe_num_max
b = top_pipe_num_min - len(before_list)
if a >= b:
final_area = before_list
else:
final_center.append(top_center)
all_node_iter = all_node_iter + list(new_all_node[top_center_index])
final_area = temp_area
break
final_center = sorted(set(final_center))
all_node_iter = sorted(set(all_node_iter))
return final_area, final_center, all_node_iter, if_end
def find_list_repeat(candidate_center, target):
repeated_list = []
for index, nums in enumerate(candidate_center):
if nums == target:
repeated_list.append(index)
return repeated_list
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 _accumulate_stage(stage_timing, stage_name, started_at):
stage_timing[stage_name] = stage_timing.get(stage_name, 0.0) + (
perf_counter() - started_at
)
def _write_last_round_candidates_csv(
csv_path,
exit_condition,
iteration_count,
similarity_mode,
candidate_details,
fallback_similarity,
):
if not csv_path:
return None
timestamp_suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
base_path, ext = os.path.splitext(csv_path)
ext = ext or ".csv"
output_path = f"{base_path}_{timestamp_suffix}{ext}"
if candidate_details is not None and len(candidate_details) > 0:
export_df = candidate_details.copy()
if export_df.index.name == "pipe_id":
export_df = export_df.reset_index()
else:
export_df = pd.DataFrame(
{
"pipe_id": [str(pipe_id) for pipe_id in fallback_similarity.index],
"final_similarity": [float(value) for value in fallback_similarity.values],
}
)
export_df["exit_condition"] = exit_condition
export_df["iterations"] = int(iteration_count)
export_df["similarity_mode"] = similarity_mode
parent_dir = os.path.dirname(output_path)
if parent_dir:
os.makedirs(parent_dir, exist_ok=True)
export_df.to_csv(output_path, index=False, encoding="utf-8-sig")
return output_path
def cal_DtoTop1(
G0, pipe_leak, located_pipe, pipe_start_node_all, pipe_end_node_all, pipe_length
):
if pipe_leak == located_pipe:
result_DtoTop1 = 0
result_DtoTop1_num = 0
else:
pipe_leak_start_node = pipe_start_node_all[pipe_leak]
pipe_leak_end_node = pipe_end_node_all[pipe_leak]
located_pipe_start_node = pipe_start_node_all[located_pipe]
located_pipe_end_node = pipe_end_node_all[located_pipe]
DtoTop1_series = pd.Series(dtype=object)
DtoTop1_num_series = pd.Series(dtype=object)
DtoTop1_series["ss"] = nx.shortest_path_length(
G0, pipe_leak_start_node, located_pipe_start_node, weight="weight"
)
DtoTop1_series["se"] = nx.shortest_path_length(
G0, pipe_leak_start_node, located_pipe_end_node, weight="weight"
)
DtoTop1_series["es"] = nx.shortest_path_length(
G0, pipe_leak_end_node, located_pipe_start_node, weight="weight"
)
DtoTop1_series["ee"] = nx.shortest_path_length(
G0, pipe_leak_end_node, located_pipe_end_node, weight="weight"
)
DtoTop1_num_series["ss"] = nx.shortest_path_length(
G0, pipe_leak_start_node, located_pipe_start_node
)
DtoTop1_num_series["se"] = nx.shortest_path_length(
G0, pipe_leak_start_node, located_pipe_end_node
)
DtoTop1_num_series["es"] = nx.shortest_path_length(
G0, pipe_leak_end_node, located_pipe_start_node
)
DtoTop1_num_series["ee"] = nx.shortest_path_length(
G0, pipe_leak_end_node, located_pipe_end_node
)
if DtoTop1_num_series.min() == 0:
result_DtoTop1_num = 1
result_DtoTop1 = DtoTop1_series.max() / 2
else:
result_DtoTop1_num = DtoTop1_num_series.min() + 1
DtoTop1_type = DtoTop1_series.argmin()
result_DtoTop1 = (
DtoTop1_series[DtoTop1_type]
+ (pipe_length[pipe_leak] + pipe_length[located_pipe]) / 2
)
return result_DtoTop1, result_DtoTop1_num
def cal_RR(located_pipe, similarity_sp):
if located_pipe in similarity_sp.index:
rank = similarity_sp.index.get_loc(located_pipe)
RR = rank / len(similarity_sp.index)
else:
RR = 1.1
return RR
def cal_cover(similarity, leak_pipe):
if leak_pipe in list(similarity.index):
cover = 1
else:
cover = 0
return cover
def cal_SD(located_pipe, real_pipe, pipe_x, pipe_y):
dx = pipe_x[located_pipe] - pipe_x[real_pipe]
dy = pipe_y[located_pipe] - pipe_y[real_pipe]
SD = math.sqrt(dx * dx + dy * dy)
return SD
def DN_search_multi_simple_add_flow_count_new(
wn,
wn_inp_path,
G0,
all_node,
node_x,
node_y,
pipe_start_node_all,
pipe_end_node_all,
pipe_diameter,
couple_node_length,
node_pipe_dic,
all_node_series,
top_group_ratio,
top_pipe_num_max,
top_pipe_num_min,
candidate_pipe_input_initial,
similarity_mode,
pressure_monitor,
pressure_predict,
pressure_normal,
pressure_leak_all,
flow_monitor,
flow_predict,
flow_normal,
flow_leak_all,
timestep_list,
max_flow,
group_basic_num,
Top_sensor_num,
if_gy,
pressure_threshold,
leak_mag,
n_workers=1,
stage_timing=None,
partition_on_full_graph=True,
visualize_partition=False,
visualize_pause_seconds=0.3,
final_candidates_csv_path=None,
):
if stage_timing is None:
stage_timing = {}
exit_condition = "unknown"
final_candidates_csv = None
iter_count = 0
all_node_iter = copy.deepcopy(all_node)
candidate_pipe_input = copy.deepcopy(candidate_pipe_input_initial) # 可能漏损管段
t1 = datetime.now()
if_flow, if_only_cos, if_only_flow = decode_mode(similarity_mode) # 定位方法
# threshold
if if_only_flow == 1:
dpressure = (flow_predict - flow_monitor).mean()
dpressure = dpressure.abs()
effective_sensor = list(dpressure.index)
else:
dpressure = (pressure_predict - pressure_monitor).mean()
dpressure = dpressure.abs()
dpressure = dpressure[dpressure > pressure_threshold]
effective_sensor = list(dpressure.index)
simulation_times = 0 # 模拟次数
if len(dpressure) > 0:
break_flag = 0
last_round_candidate_details = None
cos_h = 0
dis_h = 0
dis_f_h = 0
if_compalsive = 0
record_center_dataset = []
record_center_set = set()
# iter
while 1:
final_area = []
final_center = []
group_num = cal_group_num(candidate_pipe_input, group_basic_num)
partition_nodes = all_node if partition_on_full_graph else all_node_iter
# group 分组,得出候选漏损中心
stage_start = perf_counter()
(
candidate_center_list,
candidate_group_list,
new_all_node,
candidate_center_candidates,
) = (
metis_grouping_pipe_weight(
G0,
wn,
partition_nodes,
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,
)
)
_accumulate_stage(stage_timing, "group_partitioning", stage_start)
if visualize_partition:
visualize_metis_partition(
G0,
candidate_center_list,
candidate_group_list,
node_x,
node_y,
pipe_start_node_all,
pipe_end_node_all,
title=(
f"METIS Partition Iteration {iter_count + 1} | "
f"candidate pipes={len(candidate_pipe_input)} "
f"groups={len(candidate_group_list)}"
),
block=False,
pause_seconds=visualize_pause_seconds,
)
simulation_times = simulation_times + len(candidate_center_list)
# pick_pressure_leak
# pressure_leak = pressure_leak_all.loc[candidate_center_list].loc[:, :]
# flow_leak = flow_leak_all.loc[candidate_center_list].loc[:, :]
# —— 新增泄漏量(保持你现在的一致,或从外部传入)——
# —— 只为缺失中心补算,然后取本轮需要的中心子集 ——
stage_start = perf_counter()
pressure_leak, flow_leak, pressure_leak_all, flow_leak_all = (
_ensure_signatures_for_centers(
wn=wn,
wn_inp_path=wn_inp_path,
center_list=candidate_center_list,
pressure_leak_all=pressure_leak_all,
flow_leak_all=flow_leak_all,
timestep_list=timestep_list,
pressure_monitor=pressure_monitor,
flow_monitor=flow_monitor,
leak_mag=leak_mag,
n_workers=n_workers,
)
)
_accumulate_stage(stage_timing, "signature_for_candidates", stage_start)
# pressure_leak_f= pressure_leak.swaplevel()
# --------------------------------------------------------
add_center = []
leak_center_dict = dict()
for i in range(len(candidate_center_list)):
primary_center = candidate_center_list[i]
houxuan_center = [
center
for center in candidate_center_candidates[i]
if center != primary_center
]
candidate_group_set = set(candidate_group_list[i])
for each_center in record_center_dataset:
if (
each_center in candidate_group_set
and each_center != primary_center
):
houxuan_center.append(each_center)
add_center = add_center + houxuan_center
leak_center_dict[primary_center] = _dedupe_preserve_order(
houxuan_center + [primary_center]
)
add_center = _dedupe_preserve_order(add_center)
for each_group_centers in candidate_center_candidates:
for each_center in each_group_centers:
if each_center not in record_center_set:
record_center_dataset.append(each_center)
record_center_set.add(each_center)
for each_center in add_center:
if each_center not in record_center_set:
record_center_dataset.append(each_center)
record_center_set.add(each_center)
# --------------------------------------------------------
# --------------------------------------------------------
# if len(add_center) > 0:
# s3 = pressure_leak_all.loc[add_center]
# pressure_leak = pd.concat([pressure_leak, s3])
# s4 = flow_leak_all.loc[add_center]
# flow_leak = pd.concat([flow_leak, s4])
# --------------------------------------------------------
# 只为 add_center 里还没算过的中心补算,并与本轮中心合并
if len(add_center) > 0:
stage_start = perf_counter()
pressure_add, flow_add, pressure_leak_all, flow_leak_all = (
_ensure_signatures_for_centers(
wn=wn,
wn_inp_path=wn_inp_path,
center_list=add_center,
pressure_leak_all=pressure_leak_all,
flow_leak_all=flow_leak_all,
timestep_list=timestep_list,
pressure_monitor=pressure_monitor,
flow_monitor=flow_monitor,
leak_mag=leak_mag, # 与上面一致
n_workers=n_workers,
)
)
_accumulate_stage(
stage_timing, "signature_for_extra_centers", stage_start
)
pressure_leak = pd.concat([pressure_leak, pressure_add], axis=0)
if (flow_leak is not None) and (flow_add is not None):
flow_leak = pd.concat([flow_leak, flow_add], axis=0)
# --------------------------------------------------------
#
if len(candidate_pipe_input) < 1.2 * top_pipe_num_max / top_group_ratio:
if_compalsive = 1
cos_h, dis_h, dis_f_h = adjust_ratio(similarity_mode, cos_h, dis_h, dis_f_h)
candidate_center_list_sup = _dedupe_preserve_order(
candidate_center_list + add_center
)
stage_start = perf_counter()
similarity, cos_h, dis_h, dis_f_h, break_flag, similarity_details = (
cal_similarity_all_multi_new_sq_improve_double_lzr(
candidate_center_list_sup,
similarity_mode,
pressure_leak,
pressure_monitor,
pressure_predict,
pressure_normal,
if_flow,
if_only_cos,
if_only_flow,
flow_leak,
flow_monitor,
flow_predict,
flow_normal,
timestep_list,
Top_sensor_num,
if_gy,
effective_sensor,
cos_h,
dis_h,
dis_f_h,
if_compalsive,
max_flow,
)
)
last_round_candidate_details = similarity_details
_accumulate_stage(stage_timing, "similarity_ranking", stage_start)
if break_flag == 1:
exit_condition = "similarity_break_flag"
break
new_similarity = update_similarity(
candidate_center_list, similarity, leak_center_dict
)
if len(candidate_pipe_input) > top_pipe_num_max / top_group_ratio:
cut_ratio, new_similarity = extra_judge(new_similarity)
else:
cut_ratio = 1
stage_start = perf_counter()
final_area_t, final_center_t, all_node_new_1, if_end = (
area_output_num_ki_improve(
candidate_center_list,
candidate_group_list,
new_similarity,
new_all_node,
top_group_ratio,
top_pipe_num_max,
top_pipe_num_min,
cut_ratio,
)
)
_accumulate_stage(stage_timing, "candidate_area_selection", stage_start)
final_area = final_area + final_area_t
final_center = final_center + final_center_t
final_area = sorted(set(final_area))
final_center = sorted(set(final_center))
if if_end == 1:
exit_condition = "candidate_area_if_end"
break
elif len(candidate_pipe_input) == len(final_area):
exit_condition = "candidate_size_no_change"
break
else:
candidate_pipe_input = final_area
if not partition_on_full_graph:
all_node_iter = all_node_new_1
iter_count += 1
sys.stdout.write(
"\r"
+ "已经完成"
+ str(iter_count)
+ "次迭代计算"
+ "候选节点"
+ str(len(final_area))
+ ""
)
# if break_flag == 0:
# final_area_pipe = copy.deepcopy(final_area)
# simulation_times = simulation_times + len(final_area)
# pressure_leak_sp = pressure_leak_all.loc[final_area_pipe].loc[:, :]
# flow_leak_sp = flow_leak_all.loc[final_area_pipe].loc[:, :]
# similarity_sp, cos_h, dis_h, dis_f_h, break_flag = cal_similarity_all_multi_new_sq_improve_double_lzr(
# final_area_pipe, similarity_mode, pressure_leak_sp,
# pressure_monitor, pressure_predict, pressure_normal, if_flow,
# if_only_cos, if_only_flow,
# flow_leak_sp, flow_monitor, flow_predict, flow_normal,
# timestep_list, Top_sensor_num, if_gy, effective_sensor, cos_h, dis_h, dis_f_h, if_compalsive, max_flow)
if break_flag == 0:
final_area_pipe = list(final_area) # 确保是 list
# 只为还没算过的管段补齐 SLF(按需计算)
stage_start = perf_counter()
pressure_leak_sp, flow_leak_sp, pressure_leak_all, flow_leak_all = (
_ensure_signatures_for_centers(
wn=wn,
wn_inp_path=wn_inp_path,
center_list=final_area_pipe, # 这次要用的“最终区域里的所有管段”
pressure_leak_all=pressure_leak_all, # 累积缓存(会被更新)
flow_leak_all=flow_leak_all,
timestep_list=timestep_list,
pressure_monitor=pressure_monitor,
flow_monitor=flow_monitor,
leak_mag=leak_mag,
n_workers=n_workers,
)
)
_accumulate_stage(stage_timing, "signature_for_final_area", stage_start)
# 如果你要精确统计模拟次数,这里可以加上“本次新补的数量”,
# 做法:让 _ensure_signatures_for_centers 额外返回 need_cnt,再 simulation_times += need_cnt
stage_start = perf_counter()
(
similarity_sp,
cos_h,
dis_h,
dis_f_h,
break_flag,
similarity_details,
) = (
cal_similarity_all_multi_new_sq_improve_double_lzr(
final_area_pipe,
similarity_mode,
pressure_leak_sp,
pressure_monitor,
pressure_predict,
pressure_normal,
if_flow,
if_only_cos,
if_only_flow,
flow_leak_sp,
flow_monitor,
flow_predict,
flow_normal,
timestep_list,
Top_sensor_num,
if_gy,
effective_sensor,
cos_h,
dis_h,
dis_f_h,
if_compalsive,
max_flow,
)
)
last_round_candidate_details = similarity_details
_accumulate_stage(stage_timing, "similarity_final", stage_start)
else:
dpressure = (pressure_predict - pressure_monitor).mean()
dpressure = dpressure.abs()
simulation_times = simulation_times + len(dpressure.index)
similarity_sp = pd.Series(dtype=float)
for each_node in dpressure.index:
pipe = node_pipe_dic[each_node][0]
similarity_sp.loc[pipe] = dpressure.loc[each_node]
similarity_sp = similarity_sp.sort_values(ascending=False, kind="mergesort")
t2 = datetime.now()
final_area_pipe = []
sys.stdout.write(
"\r"
+ "已经完成"
+ str(iter_count + 1)
+ "次迭代计算"
+ "候选节点"
+ str(len(final_area_pipe))
+ ""
)
t2 = datetime.now()
dt = (t2 - t1).seconds
final_candidates_csv = _write_last_round_candidates_csv(
csv_path=final_candidates_csv_path,
exit_condition=exit_condition,
iteration_count=iter_count + 1,
similarity_mode=similarity_mode,
candidate_details=last_round_candidate_details,
fallback_similarity=similarity_sp,
)
else:
exit_condition = "no_effective_sensor_after_threshold"
dpressure = (pressure_predict - pressure_monitor).mean()
dpressure = dpressure.abs()
similarity_sp = pd.Series(dtype=float)
for each_node in dpressure.index:
pipe = node_pipe_dic[each_node][0]
similarity_sp.loc[pipe] = dpressure.loc[each_node]
similarity_sp = similarity_sp.sort_values(ascending=False, kind="mergesort")
t2 = datetime.now()
dt = (t2 - t1).seconds
final_candidates_csv = _write_last_round_candidates_csv(
csv_path=final_candidates_csv_path,
exit_condition=exit_condition,
iteration_count=0,
similarity_mode=similarity_mode,
candidate_details=None,
fallback_similarity=similarity_sp,
)
stage_timing["iterations"] = iter_count + 1 if len(dpressure) > 0 else 0
stage_timing["total_elapsed_seconds"] = float(dt)
stage_timing["exit_condition"] = exit_condition
stage_timing["final_candidates_csv"] = final_candidates_csv
return (
similarity_sp.index[0],
dt,
simulation_times,
wn,
similarity_sp,
exit_condition,
final_candidates_csv,
)
@@ -0,0 +1,563 @@
"""漏损模拟模块。"""
import math
import multiprocessing as mp
import os
import sys
import pandas as pd
import wntr
from app.algorithms._utils import _cleanup_temp_files
_PIPE2LEAKNODE = None
_SIGNATURE_WORKER_DATA = {}
def _make_temp_prefix(tag):
temp_dir = os.path.abspath(os.path.join("temp", "burst_location"))
os.makedirs(temp_dir, exist_ok=True)
safe_tag = str(tag).replace(os.sep, "_").replace(" ", "_")
return os.path.join(temp_dir, f"{safe_tag}_{os.getpid()}")
def _snapshot_hydraulic_options(wn):
options = wn.options
return {
"demand_model": options.hydraulic.demand_model,
"duration": float(options.time.duration),
"hydraulic_timestep": float(options.time.hydraulic_timestep),
"pattern_timestep": float(options.time.pattern_timestep),
"report_timestep": float(options.time.report_timestep),
"required_pressure": float(options.hydraulic.required_pressure),
"minimum_pressure": float(options.hydraulic.minimum_pressure),
}
def _apply_hydraulic_options(wn, option_values):
options = wn.options
options.hydraulic.demand_model = option_values["demand_model"]
options.time.duration = option_values["duration"]
options.time.hydraulic_timestep = option_values["hydraulic_timestep"]
options.time.pattern_timestep = option_values["pattern_timestep"]
options.time.report_timestep = option_values["report_timestep"]
options.hydraulic.required_pressure = option_values["required_pressure"]
options.hydraulic.minimum_pressure = option_values["minimum_pressure"]
def simple_add_leak(wn, leak_mag, leak_pipe):
whole_inf = dict()
leak_pipe_self = wn.get_link(leak_pipe)
pipe_diameter = leak_pipe_self.diameter
pipe_length = leak_pipe_self.length
pipe_roughness = leak_pipe_self.roughness
pipe_minor_loss = leak_pipe_self.minor_loss
# pipe_status = leak_pipe_self.status
# pipe_check_valve = leak_pipe_self.check_valve
pipe_start_node = leak_pipe_self.start_node_name
pipe_end_node = leak_pipe_self.end_node_name
# close the pipe
# leak_pipe_self.status = 'Closed'
wn.remove_link(leak_pipe)
# add the pipe
add_pipe1 = leak_pipe + "A"
add_pipe2 = leak_pipe + "B"
add_node = leak_pipe + "_"
start_n = wn.get_node(pipe_start_node)
end_n = wn.get_node(pipe_end_node)
if start_n.node_type == "Reservoir":
end_n_elevation = end_n.elevation
start_n_elevation = end_n_elevation
elif end_n.node_type == "Reservoir":
start_n_elevation = start_n.elevation
end_n_elevation = start_n_elevation
else:
end_n_elevation = end_n.elevation
start_n_elevation = start_n.elevation
elevation_self = (start_n_elevation + end_n_elevation) / 2
coordinates_self = (
(start_n.coordinates[0] + end_n.coordinates[0]) / 2,
(start_n.coordinates[1] + end_n.coordinates[1]),
)
wn.add_junction(
add_node, base_demand=0, elevation=elevation_self, coordinates=coordinates_self
)
leak_node = wn.get_node(add_node)
wn.add_pipe(
add_pipe1,
start_node_name=pipe_start_node,
end_node_name=add_node,
length=pipe_length / 2,
diameter=pipe_diameter,
roughness=pipe_roughness,
minor_loss=pipe_minor_loss,
)
wn.add_pipe(
add_pipe2,
start_node_name=pipe_end_node,
end_node_name=add_node,
length=pipe_length / 2,
diameter=pipe_diameter,
roughness=pipe_roughness,
minor_loss=pipe_minor_loss,
)
# simulation
leak_node.add_demand(base=leak_mag, pattern_name="add_leak")
whole_inf["leak_node_name"] = add_node
whole_inf["add_pipe1"] = add_pipe1
whole_inf["add_pipe2"] = add_pipe2
whole_inf["leak_pipe"] = leak_pipe
whole_inf["pipe_start_node"] = pipe_start_node
whole_inf["pipe_end_node"] = pipe_end_node
whole_inf["pipe_length"] = pipe_length
whole_inf["pipe_diameter"] = pipe_diameter
whole_inf["pipe_roughness"] = pipe_roughness
whole_inf["pipe_minor_loss"] = pipe_diameter
return wn, whole_inf, add_pipe1
def simple_recover_wn(wn, whole_inf):
leak_node = wn.get_node(whole_inf["leak_node_name"])
del leak_node.demand_timeseries_list[-1]
# update
wn.remove_link(whole_inf["add_pipe1"])
wn.remove_link(whole_inf["add_pipe2"])
wn.remove_node(whole_inf["leak_node_name"])
# open the pipe
# leak_pipe_self.status = 'Open'
wn.add_pipe(
whole_inf["leak_pipe"],
start_node_name=whole_inf["pipe_start_node"],
end_node_name=whole_inf["pipe_end_node"],
length=whole_inf["pipe_length"],
diameter=whole_inf["pipe_diameter"],
roughness=whole_inf["pipe_roughness"],
minor_loss=whole_inf["pipe_minor_loss"],
)
return wn
def disable_all_controls_temporarily(wn):
"""返回(控制名, 控制对象)的列表,之后可用 restore_controls 还原。"""
removed = []
# WNTR 的控制都在 wn.control_name_list / wn.get_control / wn.remove_control
for cname in list(wn.control_name_list):
ctrl = wn.get_control(cname)
removed.append((cname, ctrl))
wn.remove_control(cname)
return removed
def restore_controls(wn, removed):
"""把先前禁用的控制全部加回去。"""
for cname, ctrl in removed:
wn.add_control(cname, ctrl)
def set_pipe2leaknode_mapping(mapping):
global _PIPE2LEAKNODE
_PIPE2LEAKNODE = mapping
def _get_or_create_leak_demand_ts(leak_node):
"""
返回:泄漏专用 demand 的下标 idx。
若不存在,以 category='leak' 新建一条 base=0.0 的 demand。
"""
# 先尝试找到已有的 'leak' 分类
for i, ts in enumerate(leak_node.demand_timeseries_list):
# WNTR 的 Demand object 存在 category 属性
if getattr(ts, "category", None) == "leak":
return i
# 没有则新建(base=0.0,后续临时改 base_value
leak_node.add_demand(base=0.0, pattern_name=None, category="leak")
return len(leak_node.demand_timeseries_list) - 1
def ensure_mid_node(wn, leak_pipe):
add_pipe1 = f"{leak_pipe}A"
add_pipe2 = f"{leak_pipe}B"
add_node = f"{leak_pipe}__mid"
if add_node in wn.node_name_list:
return add_node
if leak_pipe in wn.link_name_list:
leak_pipe_self = wn.get_link(leak_pipe)
pipe_diameter = leak_pipe_self.diameter
pipe_length = leak_pipe_self.length
pipe_roughness = leak_pipe_self.roughness
pipe_minor_loss = leak_pipe_self.minor_loss
pipe_start_node = leak_pipe_self.start_node_name
pipe_end_node = leak_pipe_self.end_node_name
start_n = wn.get_node(pipe_start_node)
end_n = wn.get_node(pipe_end_node)
if start_n.node_type == "Reservoir":
end_elev = end_n.elevation
start_elev = end_elev
elif end_n.node_type == "Reservoir":
start_elev = start_n.elevation
end_elev = start_elev
else:
end_elev = end_n.elevation
start_elev = start_n.elevation
elev_mid = (start_elev + end_elev) / 2.0
x_mid = (start_n.coordinates[0] + end_n.coordinates[0]) / 2.0
y_mid = (start_n.coordinates[1] + end_n.coordinates[1]) / 2.0
wn.remove_link(leak_pipe)
wn.add_junction(
add_node, base_demand=0.0, elevation=elev_mid, coordinates=(x_mid, y_mid)
)
wn.add_pipe(
add_pipe1,
start_node_name=pipe_start_node,
end_node_name=add_node,
length=pipe_length / 2.0,
diameter=pipe_diameter,
roughness=pipe_roughness,
minor_loss=pipe_minor_loss,
)
wn.add_pipe(
add_pipe2,
start_node_name=add_node,
end_node_name=pipe_end_node,
length=pipe_length / 2.0,
diameter=pipe_diameter,
roughness=pipe_roughness,
minor_loss=pipe_minor_loss,
)
return add_node
# 若 A/B 已存在但中点不在,建议确认网络一致性
raise KeyError(f"Cannot ensure mid node for pipe '{leak_pipe}'.")
def leak_simulation_pipe_dd_multi_pf(
wn, leak_mag, leak_pipe, sensor_name, file_prefix=None
):
"""
优化版:
- 不再 remove/add link/node
- 直接在预插入的中点泄漏节点上设置 base_demand = leak_mag;仿真后设回 0
"""
wn.options.hydraulic.demand_model = "DD"
# 确保中点节点存在
leak_node_name = ensure_mid_node(wn, leak_pipe)
leak_node = wn.get_node(leak_node_name)
# 拿到泄漏专用的 demand time-series 下标
leak_idx = _get_or_create_leak_demand_ts(leak_node)
ts_obj = leak_node.demand_timeseries_list[leak_idx]
# 记录原值(通常是 0.0
orig_base = ts_obj.base_value
try:
# 打开泄漏:只改 base_value,不碰 base_demand(只读)
ts_obj.base_value = float(leak_mag)
# 仿真
sim = wntr.sim.EpanetSimulator(wn)
if file_prefix is None:
results = sim.run_sim()
else:
results = sim.run_sim(file_prefix=file_prefix)
# 输出(保持列顺序)
pressure_output = results.node["pressure"].loc[:, sensor_name]
# flow_output = results.link['flowrate'].loc[:, sensor_f_name]
return wn, pressure_output
finally:
# 关闭泄漏:还原 base_value
ts_obj.base_value = orig_base
if file_prefix is not None:
_cleanup_temp_files(file_prefix)
def prepare_leak_infrastructure(wn, candidate_pipes):
"""
把 candidate_pipes 每条管段切成两段,并在中点插入一个泄漏节点(base_demand=0)。
返回一个映射:pipe_id -> leak_node_name
注意:只做一次;后续仿真通过在该节点设置 base_demand 实现“打开泄漏”,结束后恢复为 0。
"""
pipe2leaknode = {}
for leak_pipe in candidate_pipes:
if leak_pipe in pipe2leaknode:
continue
leak_pipe_self = wn.get_link(leak_pipe)
pipe_diameter = leak_pipe_self.diameter
pipe_length = leak_pipe_self.length
pipe_roughness = leak_pipe_self.roughness
pipe_minor_loss = leak_pipe_self.minor_loss
pipe_start_node = leak_pipe_self.start_node_name
pipe_end_node = leak_pipe_self.end_node_name
# 计算中点高程/坐标(与原逻辑一致)
start_n = wn.get_node(pipe_start_node)
end_n = wn.get_node(pipe_end_node)
if start_n.node_type == "Reservoir":
end_elev = end_n.elevation
start_elev = end_elev
elif end_n.node_type == "Reservoir":
start_elev = start_n.elevation
end_elev = start_elev
else:
end_elev = end_n.elevation
start_elev = start_n.elevation
elev_mid = (start_elev + end_elev) / 2.0
x_mid = (start_n.coordinates[0] + end_n.coordinates[0]) / 2.0
y_mid = (start_n.coordinates[1] + end_n.coordinates[1]) / 2.0
# 先删原管,再加中点与两段半长管(只做一次)
wn.remove_link(leak_pipe)
add_pipe1 = f"{leak_pipe}A"
add_pipe2 = f"{leak_pipe}B"
add_node = f"{leak_pipe}__mid" # 唯一命名,后面直接用它当泄漏节点
wn.add_junction(
add_node, base_demand=0.0, elevation=elev_mid, coordinates=(x_mid, y_mid)
)
wn.add_pipe(
add_pipe1,
start_node_name=pipe_start_node,
end_node_name=add_node,
length=pipe_length / 2.0,
diameter=pipe_diameter,
roughness=pipe_roughness,
minor_loss=pipe_minor_loss,
)
wn.add_pipe(
add_pipe2,
start_node_name=add_node,
end_node_name=pipe_end_node,
length=pipe_length / 2.0,
diameter=pipe_diameter,
roughness=pipe_roughness,
minor_loss=pipe_minor_loss,
)
pipe2leaknode[leak_pipe] = add_node
return pipe2leaknode
def normal_simulation_pf(
wn, drive_mode, sensor_name, sensor_f_name, inp_time, require_p, minimum_p
):
# inp_time = 0
if drive_mode == "PDD": # 需水量根据节点压力动态调整
wn.options.hydraulic.demand_model = "PDD"
wn.options.hydraulic.required_pressure = require_p
wn.options.hydraulic.minimum_pressure = minimum_p
elif drive_mode == "DD": # 需水量固定,与压力无关
wn.options.hydraulic.demand_model = "DD"
sim = wntr.sim.EpanetSimulator(wn)
results = sim.run_sim()
pressure_all = results.node["pressure"][sensor_name]
pressure = pressure_all.iloc[inp_time]
demand_all = results.node["demand"]
demand = demand_all.iloc[inp_time]
sum_demand = cal_sum_demand(demand)
flow_all = results.link["flowrate"][sensor_f_name]
flow = flow_all.iloc[inp_time]
top_sensor = pressure.idxmin()
basic_p = results.node["pressure"]
basic_p = basic_p.iloc[inp_time]
return pressure, flow, basic_p, top_sensor, sum_demand
def normal_simulation_multi_pf(
wn, drive_mode, sensor_name, sensor_f_name, require_p, minimum_p
):
# inp_time = 0
if drive_mode == "PDD":
wn.options.hydraulic.demand_model = "PDD"
wn.options.hydraulic.required_pressure = require_p
wn.options.hydraulic.minimum_pressure = minimum_p
elif drive_mode == "DD":
wn.options.hydraulic.demand_model = "DD"
sim = wntr.sim.EpanetSimulator(wn)
results = sim.run_sim()
pressure_all = results.node["pressure"][sensor_name]
pressure = pressure_all
demand_all = results.node["demand"]
demand = demand_all
flow = results.link["flowrate"][sensor_f_name]
sum_demand = pd.Series(dtype=object)
for i in range(len(demand.index)):
sum_demand[str(demand.index[i])] = cal_sum_demand(demand.iloc[i])
if type(pressure) == pd.core.series.Series:
top_sensor = pressure.idxmin()
else:
mean_pressure = pressure.mean()
top_sensor = mean_pressure.idxmin()
basic_p = results.node["pressure"]
return pressure, flow, basic_p, top_sensor, sum_demand
def simple_simulation_pf(wn, sensor_name, sensor_f_name, leak_pipe, add_pipe1):
sim = wntr.sim.EpanetSimulator(wn)
results = sim.run_sim()
pressure_all = results.node["pressure"][sensor_name]
if len(leak_pipe) > 0 and leak_pipe in sensor_f_name:
f_sensor_name = [add_pipe1 if i == leak_pipe else i for i in sensor_f_name]
flow_all = results.link["flowrate"][f_sensor_name]
flow_all.columns = sensor_f_name
else:
flow_all = results.link["flowrate"][sensor_f_name]
return pressure_all, flow_all
def cal_sum_demand(demand):
sum_demand = 0
for i in range(len(demand)):
if demand.iloc[i] > 0:
sum_demand += demand.iloc[i]
return sum_demand
def cal_signature_pipe_multi_pf(
wn,
leak_mag,
candidate_center,
timestep_list,
sensor_name,
n_workers=1,
wn_inp_path=None,
):
candidate_center_num = len(candidate_center)
pressure_leak = pd.DataFrame(
index=pd.MultiIndex.from_product([candidate_center, timestep_list]),
columns=sensor_name,
)
# flow_leak = pd.DataFrame(index=pd.MultiIndex.from_product([candidate_center, timestep_list]),
# columns=sensor_f_name)
pressure_leak = pressure_leak.sort_index()
# flow_leak = flow_leak.sort_index()
can_parallel = (
n_workers > 1
and candidate_center_num > 1
and wn_inp_path is not None
and len(str(wn_inp_path)) > 0
)
if can_parallel:
option_values = _snapshot_hydraulic_options(wn)
worker_count = min(n_workers, candidate_center_num)
start_methods = mp.get_all_start_methods()
context_name = "spawn" if "spawn" in start_methods else start_methods[0]
with mp.get_context(context_name).Pool(
processes=worker_count,
initializer=_signature_worker_init,
initargs=(
str(wn_inp_path),
float(leak_mag),
list(sensor_name),
option_values,
list(candidate_center),
),
) as pool:
for i, (center_name, pressure_array) in enumerate(
pool.imap(_signature_worker_run_center, candidate_center)
):
pressure_leak.loc[(center_name, slice(None)), :] = pressure_array
sys.stdout.write("\r" + "已经完成计算" + str(i + 1) + "个特征中心")
else:
# Pre-insert all mid-nodes so every simulation sees the same topology
for center in candidate_center:
ensure_mid_node(wn, center)
for i in range(candidate_center_num):
temp_prefix = _make_temp_prefix(f"sig_{i}")
wn, pressure_output = leak_simulation_pipe_dd_multi_pf(
wn,
leak_mag,
candidate_center[i],
sensor_name,
file_prefix=temp_prefix,
)
# leak_or_not_list.append(leak_or_not)
pressure_leak.loc[(candidate_center[i], slice(None)), :] = (
pressure_output.to_numpy()
)
# flow_leak.loc[candidate_center[i]].loc[:, :] = flow_output
sys.stdout.write("\r" + "已经完成计算" + str(i + 1) + "个特征中心")
return pressure_leak, candidate_center
def _signature_worker_init(
inp_path, leak_mag, sensor_name, option_values, candidate_centers=None
):
global _SIGNATURE_WORKER_DATA
wn = wntr.network.WaterNetworkModel(inp_path)
_apply_hydraulic_options(wn, option_values)
# Pre-insert ALL mid-nodes so every simulation runs on the same topology,
# regardless of which worker handles which task.
if candidate_centers is not None:
for center in candidate_centers:
ensure_mid_node(wn, center)
_SIGNATURE_WORKER_DATA = {
"wn": wn,
"leak_mag": leak_mag,
"sensor_name": sensor_name,
}
def _signature_worker_run_center(center_name):
data = _SIGNATURE_WORKER_DATA
temp_prefix = _make_temp_prefix(f"sig_worker_{center_name}")
_, pressure_output = leak_simulation_pipe_dd_multi_pf(
data["wn"],
data["leak_mag"],
center_name,
data["sensor_name"],
file_prefix=temp_prefix,
)
return center_name, pressure_output.to_numpy()
def pick_pipe(all_pipes, pipe_diameter, limited_diameter):
candidate_pipe = []
for each_pipe in all_pipes:
if pipe_diameter[each_pipe] >= limited_diameter:
candidate_pipe.append(each_pipe)
return candidate_pipe
def cal_possible_pipe(leak_flow, all_pipe, pipe_diameter):
basic_pressure = 10 # 基础压力
discharge_coeff = 0.6 # 经验系数
break_area_ratio = 1 # 爆管面积比 0.5 1.25
break_area = leak_flow / (
discharge_coeff * math.sqrt(2 * basic_pressure * 9.81)
) # 爆管面积 m3/h
"""break_area_diameter = math.sqrt(4 * break_area / math.pi)
min_diameter = (math.ceil(1000 * break_area_diameter / break_area_ratio)) / 1000"""
break_area_diameter = math.sqrt(
4 * break_area / math.pi / break_area_ratio
) # 爆管直径
min_diameter = (math.ceil(1000 * break_area_diameter)) / 1000 # 向上取整
new_all_pipe = pick_pipe(all_pipe, pipe_diameter, min_diameter)
return new_all_pipe, min_diameter
def extract_links(data, link_types, direction):
return [
link
for res_data in data.values()
for link_type in link_types
for link in res_data[link_type][direction]
]
@@ -0,0 +1,238 @@
import logging
from multiprocessing import cpu_count
from pathlib import Path
from typing import Any, Iterable
import pandas as pd
from app.algorithms.burst_localization import leak_signature
from .candidate_ranking import (
DN_search_multi_simple_add_flow_count_new,
)
from .topology_model import (
_build_node_pipe_maps,
cal_node_coordinate,
construct_graph,
load_inp,
read_inf_inp,
read_inf_inp_other,
)
DEFAULT_N_WORKERS = max(1, min(cpu_count() - 1, 4))
# DEFAULT_N_WORKERS = max(1, cpu_count() - 1)
logger = logging.getLogger(__name__)
def _align_scada_series(
series: pd.Series, ids: Iterable[str], series_name: str
) -> pd.Series:
ids = [str(item) for item in ids]
aligned = series.copy()
aligned.index = aligned.index.map(str)
missing_ids = [item for item in ids if item not in aligned.index]
if missing_ids:
preview = ", ".join(missing_ids[:10])
raise ValueError(f"{series_name} missing IDs: {preview}")
aligned = pd.to_numeric(aligned.loc[ids], errors="coerce")
invalid_ids = aligned[aligned.isna()].index.tolist()
if invalid_ids:
preview = ", ".join(invalid_ids[:10])
raise ValueError(
f"{series_name} contains non-numeric values for IDs: {preview}"
)
return aligned
def _validate_flow_inputs(
flow_scada_ids: list[str] | None,
burst_flow: pd.Series | None,
normal_flow: pd.Series | None,
) -> tuple[bool, list[str]]:
has_any_flow = any(
value is not None for value in [flow_scada_ids, burst_flow, normal_flow]
)
has_all_flow = all(
value is not None for value in [flow_scada_ids, burst_flow, normal_flow]
)
if has_any_flow and not has_all_flow:
raise ValueError(
"flow_scada_ids, burst_flow, and normal_flow must be provided together."
)
if not has_all_flow:
return False, []
flow_ids = [str(item) for item in (flow_scada_ids or [])]
if len(flow_ids) == 0:
raise ValueError("flow_scada_ids cannot be empty when flow data is provided.")
return True, flow_ids
def _build_top_candidates(similarity_series: pd.Series) -> list[dict[str, Any]]:
top_series = similarity_series.iloc[:10]
return [
{"pipe_id": str(pipe_id), "similarity": float(score)}
for pipe_id, score in top_series.items()
]
def run_burst_location(
wn_inp_path: str,
pressure_scada_ids: list[str],
burst_pressure: pd.Series,
normal_pressure: pd.Series,
burst_leakage: float,
flow_scada_ids: list[str] | None = None,
burst_flow: pd.Series | None = None,
normal_flow: pd.Series | None = None,
min_dpressure: float = 2.0,
basic_pressure: float = 10.0,
n_workers: int = DEFAULT_N_WORKERS,
partition_on_full_graph: bool = True,
visualize_partition: bool = False,
visualize_pause_seconds: float = 0.3,
final_candidates_csv_path: (
str | None
) = "temp/burst_location/final_round_candidates.csv",
) -> dict[str, Any]:
if pressure_scada_ids is None or len(pressure_scada_ids) == 0:
raise ValueError("pressure_scada_ids cannot be empty.")
if burst_pressure is None or normal_pressure is None:
raise ValueError("burst_pressure and normal_pressure are required.")
has_all_flow, flow_ids = _validate_flow_inputs(
flow_scada_ids=flow_scada_ids,
burst_flow=burst_flow,
normal_flow=normal_flow,
)
inp_path = Path(wn_inp_path)
wn = load_inp(
inp_name=inp_path.name,
inp_location=str(inp_path.parent) + "/",
inp_time=0,
driven_mode="PDD",
require_p=float(basic_pressure),
minimum_p=0.0,
)
(
all_node,
_,
node_coordinates,
all_pipe,
_,
_,
pipe_length,
pipe_diameter,
) = read_inf_inp(wn)
candidate_pipe, _ = leak_signature.cal_possible_pipe(
burst_leakage, all_pipe, pipe_diameter
)
_, pipe_start_node_all, pipe_end_node_all = read_inf_inp_other(wn)
node_x, node_y = cal_node_coordinate(all_node, node_coordinates)
G0 = construct_graph(wn)
node_pipe_dic, couple_node_length = _build_node_pipe_maps(
all_node,
all_pipe,
pipe_start_node_all,
pipe_end_node_all,
pipe_length,
)
all_node_series = pd.Series(range(len(all_node)), index=all_node)
pressure_ids = [str(item) for item in pressure_scada_ids]
normal_pressure_aligned = _align_scada_series(
normal_pressure, pressure_ids, "normal_pressure"
)
burst_pressure_aligned = _align_scada_series(
burst_pressure, pressure_ids, "burst_pressure"
)
pressure_normal = normal_pressure_aligned.to_frame().T
pressure_monitor = burst_pressure_aligned.to_frame().T
pressure_predict = pressure_normal.copy()
timestep_list = list(pressure_normal.index)
if has_all_flow:
normal_flow_aligned = _align_scada_series(normal_flow, flow_ids, "normal_flow")
burst_flow_aligned = _align_scada_series(burst_flow, flow_ids, "burst_flow")
flow_normal = normal_flow_aligned.to_frame().T
flow_monitor = burst_flow_aligned.to_frame().T
flow_predict = flow_normal.copy()
similarity_mode = "CDF"
max_flow = flow_normal.iloc[0, :].abs()
else:
flow_normal = pd.DataFrame(index=timestep_list)
flow_monitor = pd.DataFrame(index=timestep_list)
flow_predict = pd.DataFrame(index=timestep_list)
similarity_mode = "CAD_new_gy"
max_flow = pd.Series(dtype=float)
stage_timing: dict[str, Any] = {}
try:
(
located_pipe,
elapsed_seconds,
simulation_times,
_,
similarity_series,
exit_condition,
final_candidates_csv,
) = DN_search_multi_simple_add_flow_count_new(
wn=wn,
wn_inp_path=str(inp_path),
G0=G0,
all_node=all_node,
node_x=node_x,
node_y=node_y,
pipe_start_node_all=pipe_start_node_all,
pipe_end_node_all=pipe_end_node_all,
pipe_diameter=pipe_diameter,
couple_node_length=couple_node_length,
node_pipe_dic=node_pipe_dic,
all_node_series=all_node_series,
top_group_ratio=0.3,
top_pipe_num_max=80,
top_pipe_num_min=10,
candidate_pipe_input_initial=candidate_pipe,
similarity_mode=similarity_mode,
pressure_monitor=pressure_monitor,
pressure_predict=pressure_predict,
pressure_normal=pressure_normal,
pressure_leak_all=None,
flow_monitor=flow_monitor,
flow_predict=flow_predict,
flow_normal=flow_normal,
flow_leak_all=None,
timestep_list=timestep_list,
max_flow=max_flow,
group_basic_num=30,
Top_sensor_num=min(5, len(pressure_ids)),
if_gy=0,
pressure_threshold=float(min_dpressure),
leak_mag=float(burst_leakage),
n_workers=max(1, int(n_workers)),
stage_timing=stage_timing,
partition_on_full_graph=partition_on_full_graph,
visualize_partition=visualize_partition,
visualize_pause_seconds=visualize_pause_seconds,
final_candidates_csv_path=final_candidates_csv_path,
)
except Exception as exc:
logger.exception("Burst location algorithm execution failed.")
raise RuntimeError(f"Failed to run burst location algorithm: {exc}") from exc
return {
"located_pipe": located_pipe,
"burst_leakage": float(burst_leakage),
"elapsed_seconds": elapsed_seconds,
"simulation_times": int(simulation_times),
"top_candidates": _build_top_candidates(similarity_series),
"similarity_mode": similarity_mode,
"exit_condition": exit_condition,
"final_candidates_csv": final_candidates_csv,
"stage_timing_seconds": stage_timing,
}
@@ -0,0 +1,858 @@
"""相似性计算模块。"""
import math
import numpy as np
import pandas as pd
def cal_similarity_simple_return_dd(
similarity_mode,
monitor_p,
predict_p,
normal_p,
leak_p,
monitor_p_all,
predict_p_all,
normal_p_all,
leak_p_all,
important_sensor,
mean_dpressure,
dpressure_std,
dpressure_std_all,
if_gy=0,
cos_or_flow=1,
):
# cos_or_flow 用于 CAF
dpressure_s = normal_p - leak_p
dpressure = predict_p - monitor_p
act_dpressure = pd.Series(dtype=object)
for i in range(len(leak_p.index)):
if dpressure_std.iloc[i] > -200: # 0.001:
if if_gy == 1:
act_dpressure[leak_p.index[i]] = (
leak_p.iloc[i] - monitor_p.iloc[i]
) / dpressure_std.iloc[i]
else:
act_dpressure[leak_p.index[i]] = leak_p.iloc[i] - monitor_p.iloc[i]
if similarity_mode == "COS" or (similarity_mode == "CAF" and cos_or_flow == 1):
"""if leak_p.min()<0:
none_flag = 1
similarity_cos = 0
similarity_dis = 0
else:"""
none_flag = 0
sensor_for_cos = sorted(
set(dpressure_s.index).intersection(set(act_dpressure.index))
)
"""if len(dpressure_s) ==0 or len(dpressure) ==0:
jj=9
else:"""
try:
s1 = np.dot(
np.transpose(dpressure_s.loc[sensor_for_cos]),
dpressure.loc[sensor_for_cos],
)
s2 = np.linalg.norm(dpressure_s.loc[sensor_for_cos]) * np.linalg.norm(
dpressure.loc[sensor_for_cos]
)
if s2 == 0:
s2 = s2 + 0.0001
similarity_cos = s1 / s2
similarity_dis = 0
except Exception as e:
print(dpressure_s)
print(sensor_for_cos)
print(act_dpressure)
print(dpressure_std)
print(dpressure)
elif similarity_mode == "DIS" or (similarity_mode == "CAF" and cos_or_flow == 2):
"""if leak_p.min()<0:
none_flag = 1
else:"""
none_flag = 0
important_sensor = sorted(
set(important_sensor).intersection(set(act_dpressure.index))
)
part_dpressure = dpressure_s[important_sensor] - dpressure[important_sensor]
similarity_pre_DIS = np.linalg.norm(part_dpressure)
# similarity_pre_DIS_later = 1 / (1 + similarity_pre_DIS)
similarity_dis = similarity_pre_DIS
similarity_cos = 0
elif similarity_mode == "CAD_new":
act_dpressure = leak_p - monitor_p
"""if leak_p.min() < 0:
none_flag = 1
similarity_cos = 0
similarity_dis =0
else:"""
none_flag = 0
# cos
s1 = np.dot(np.transpose(dpressure_s), dpressure)
s2 = np.linalg.norm(dpressure_s) * np.linalg.norm(dpressure)
if s2 == 0:
s2 = s2 + 0.0001
similarity_cos = s1 / s2
# DIS
part_dpressure = act_dpressure.loc[important_sensor]
similarity_pre_DIS = np.linalg.norm(part_dpressure)
similarity_pre_DIS_later = 1 / (1 + similarity_pre_DIS)
similarity_dis = similarity_pre_DIS
elif similarity_mode == "CAD_new_gy" or similarity_mode == "CDF":
# cos
sensor_for_cos = sorted(
set(dpressure_s.index).intersection(set(act_dpressure.index))
)
if len(sensor_for_cos) == 0 and len(dpressure_s) == 0:
similarity_cos = 0
elif len(sensor_for_cos) == 0 and len(dpressure_s) > 0:
sensor_for_cos = list(dpressure_s.index)
none_flag = 0
s1 = np.dot(
np.transpose(dpressure_s.loc[sensor_for_cos]),
dpressure.loc[sensor_for_cos],
)
s2 = np.linalg.norm(dpressure_s.loc[sensor_for_cos]) * np.linalg.norm(
dpressure.loc[sensor_for_cos]
)
if s2 == 0:
s2 = s2 + 0.0001
similarity_cos = s1 / s2
else:
none_flag = 0
s1 = np.dot(
np.transpose(dpressure_s.loc[sensor_for_cos]),
dpressure.loc[sensor_for_cos],
)
s2 = np.linalg.norm(dpressure_s.loc[sensor_for_cos]) * np.linalg.norm(
dpressure.loc[sensor_for_cos]
)
if s2 == 0:
s2 = s2 + 0.0001
similarity_cos = s1 / s2
# DIS
important_sensor_new = sorted(
set(important_sensor).intersection(set(act_dpressure.index))
)
if len(important_sensor_new) == 0:
important_sensor_new = important_sensor
act_dpressure = pd.Series(dtype=object)
for i in range(len(leak_p_all.index)):
# if dpressure_std.iloc [i] > -200: # 0.001:
if if_gy == 1:
act_dpressure[leak_p_all.index[i]] = (
leak_p_all.iloc[i] - monitor_p_all.iloc[i]
) / dpressure_std_all.iloc[i]
else:
act_dpressure[leak_p_all.index[i]] = (
leak_p_all.iloc[i] - monitor_p_all.iloc[i]
)
# part_dpressure = act_dpressure.loc[important_sensor_new]
part_dpressure = (
dpressure.loc[important_sensor_new] - dpressure_s.loc[important_sensor_new]
)
similarity_pre_DIS = np.linalg.norm(part_dpressure) ## chang test
# part_dpressure = dpressure_s.loc[important_sensor]-dpressure.loc[important_sensor]
# similarity_pre_DIS = np.linalg.norm(part_dpressure)
# similarity_pre_DIS_later = 1 / (1 + similarity_pre_DIS)
similarity_dis = similarity_pre_DIS
elif similarity_mode == "OF":
# cos
similarity_cos = 0
none_flag = 0
# DIS
important_sensor_new = sorted(
set(important_sensor).intersection(set(act_dpressure.index))
)
if len(important_sensor_new) == 0:
important_sensor_new = important_sensor
act_dpressure = pd.Series(dtype=object)
for i in range(len(leak_p_all.index)):
# if dpressure_std.iloc [i] > -200: # 0.001:
if if_gy == 1:
act_dpressure[leak_p_all.index[i]] = (
leak_p_all.iloc[i] - monitor_p_all.iloc[i]
) / dpressure_std_all.iloc[i]
else:
act_dpressure[leak_p_all.index[i]] = (
leak_p_all.iloc[i] - monitor_p_all.iloc[i]
)
# part_dpressure = act_dpressure.loc[important_sensor_new]
part_dpressure = (
dpressure.loc[important_sensor_new] - dpressure_s.loc[important_sensor_new]
)
similarity_pre_DIS = np.linalg.norm(part_dpressure) ## chang test
# part_dpressure = dpressure_s.loc[important_sensor]-dpressure.loc[important_sensor]
# similarity_pre_DIS = np.linalg.norm(part_dpressure)
# similarity_pre_DIS_later = 1 / (1 + similarity_pre_DIS)
similarity_dis = similarity_pre_DIS
return similarity_cos, similarity_dis, none_flag
def adjust(
similarity_cos,
similarity_dis,
record_success_candidate,
record_success_no_candidate,
):
if len(record_success_no_candidate) > 0:
for each in record_success_no_candidate:
similarity_cos[each] = similarity_cos[record_success_candidate].min() * 0.9
similarity_dis[each] = similarity_dis[record_success_candidate].max() * 1.1
return similarity_cos, similarity_dis
def cal_sq_all_multi(
similarity_cos,
similarity_dis,
similarity_f,
candidate_pipe,
timestep_list_spc,
if_flow,
if_only_cos,
if_only_flow,
cos_h_input,
dis_h_input,
dis_f_h_input,
if_compalsive,
cos_sensor_num,
flow_sensor_num,
):
"""融合多种相似性并输出按时刻与候选管段组织的综合相似度。
该函数会根据模式开关(是否仅流量、是否仅 COS、是否包含流量)对
`similarity_cos`、`similarity_dis`、`similarity_f` 做标准化,并计算
权重 `sq_cos/sq_dis/sq_f` 后进行加权融合。
Args:
similarity_cos: 压力余弦相似性(DataFrame/Series,通常为时刻 x 候选管段)。
similarity_dis: 压力距离相似性(DataFrame/Series,通常为时刻 x 候选管段)。
similarity_f: 流量距离相似性(DataFrame/Series,通常为时刻 x 候选管段)。
candidate_pipe: 候选管段列表,用于输出列索引。
timestep_list_spc: 时刻列表,用于输出行索引。
if_flow: 是否启用流量相似性(1 启用,0 禁用)。
if_only_cos: 相似性模式标识(0: COS+DIS;1: COS;其他值按分支定义处理)。
if_only_flow: 是否仅使用流量相似性(1 是,0 否)。
cos_h_input: 外部给定的 COS 权重(强制权重模式下使用)。
dis_h_input: 外部给定的 DIS 权重(强制权重模式下使用)。
dis_f_h_input: 外部给定的流量权重(强制权重模式下使用)。
if_compalsive: 是否使用外部强制权重(1 使用输入权重,0 自动计算权重)。
cos_sensor_num: 压力传感器数量,用于权重调整。
flow_sensor_num: 流量传感器数量,用于权重调整。
Returns:
tuple[pd.DataFrame | pd.Series, float, float, float]:
- output_similarity_pd: 综合相似性结果。
- sq_cos: 最终 COS 权重。
- sq_dis: 最终 DIS 权重。
- sq_f: 最终流量权重。
"""
if if_only_flow == 1:
similarity_f, h_f = cal_sq_single_array(
similarity_f.values.reshape((-1, 1)), if_direct=2
)
sq_cos = 0
sq_dis = 0
sq_f = 1
similarity_all = similarity_f * sq_f
output_similarity = similarity_all.reshape((-1, len(candidate_pipe)))
output_similarity_pd = pd.DataFrame(
output_similarity, index=timestep_list_spc, columns=candidate_pipe
)
else:
if if_only_cos == 0:
if if_flow == 1:
# standerdize
similarity_cos, h_cos = cal_sq_single_array(
similarity_cos.values.reshape((-1, 1)), if_direct=1
)
similarity_dis, h_dis = cal_sq_single_array(
similarity_dis.values.reshape((-1, 1)), if_direct=2
)
similarity_f, h_f = cal_sq_single_array(
similarity_f.values.reshape((-1, 1)), if_direct=2
)
if if_compalsive == 1:
sq_cos = cos_h_input
sq_dis = dis_h_input
sq_f = dis_f_h_input
else:
"""sq_cos = h_cos/(h_cos +h_dis +h_f )
sq_dis = h_dis/(h_cos +h_dis +h_f )
sq_f = h_f/(h_cos +h_dis +h_f )"""
sq_cos, sq_dis, sq_f = add_weight_for_SQ(
h_cos, h_dis, h_f, cos_sensor_num, flow_sensor_num
)
"""if cos_sensor_num == 2 and sq_cos>0.2:
sq_cos = 0.2
sq_dis = 0.8*h_dis / (h_dis + h_f)
sq_f = 0.8*h_f / (h_dis + h_f)
if cos_sensor_num == 1 and sq_dis > 0.3:
sq_cos = 0.1
sq_dis = 0.3
sq_f = 0.6"""
sq_cos, sq_dis, sq_f = adjust_ratio("CDF", sq_cos, sq_dis, sq_f)
if cos_sensor_num <= 1:
sq_cos = 0
# similarity
similarity_all = (
similarity_cos * sq_cos
+ similarity_dis * sq_dis
+ similarity_f * sq_f
)
output_similarity = similarity_all.reshape((-1, len(candidate_pipe)))
output_similarity_pd = pd.DataFrame(
output_similarity, index=timestep_list_spc, columns=candidate_pipe
)
else:
# standerdize
similarity_cos, h_cos = cal_sq_single_array(
similarity_cos.values.reshape((-1, 1)), if_direct=1
)
similarity_dis, h_dis = cal_sq_single_array(
similarity_dis.values.reshape((-1, 1)), if_direct=2
)
if if_compalsive == 1:
sq_cos = cos_h_input
sq_dis = dis_h_input
else:
sq_cos = h_cos / (h_cos + h_dis)
sq_dis = h_dis / (h_cos + h_dis)
if cos_sensor_num == 2 and sq_cos > 0.5:
sq_cos = 0.5
sq_dis = 0.5
sq_cos, sq_dis, sq_f = adjust_ratio("CAD_new_gy", sq_cos, sq_dis, 0)
sq_f = 0
# similarity
similarity_all = similarity_cos * sq_cos + similarity_dis * sq_dis
output_similarity = similarity_all.reshape((-1, len(candidate_pipe)))
output_similarity_pd = pd.DataFrame(
output_similarity, index=timestep_list_spc, columns=candidate_pipe
)
elif if_only_cos == 1:
if if_flow == 1:
# standerdize
similarity_cos, h_cos = cal_sq_single_array(
similarity_cos.values.reshape((-1, 1)), if_direct=1
)
similarity_f, h_f = cal_sq_single_array(
similarity_f.values.reshape((-1, 1)), if_direct=2
)
if if_compalsive == 1:
sq_cos = cos_h_input
sq_f = dis_f_h_input
else:
sq_cos = h_cos / (h_cos + h_f)
sq_f = h_f / (h_cos + h_f)
sq_cos, sq_dis, sq_f = adjust_ratio("CAF", sq_cos, 0, sq_f)
sq_dis = 0
# similarity
similarity_all = similarity_cos * sq_cos + similarity_f * sq_f
output_similarity = similarity_all.reshape((-1, len(candidate_pipe)))
output_similarity_pd = pd.DataFrame(
output_similarity, index=timestep_list_spc, columns=candidate_pipe
)
else:
sq_cos = cos_h_input
sq_dis = dis_h_input
sq_f = dis_f_h_input
output_similarity_pd = similarity_cos
else:
sq_cos = cos_h_input
sq_dis = dis_h_input
sq_f = dis_f_h_input
output_similarity_pd = 1 / (similarity_dis + 1)
return output_similarity_pd, sq_cos, sq_dis, sq_f
def add_weight_for_SQ(h_cos, h_dis, h_f, sensor_cos_num, sensor_f_num):
h_f_new = h_f * sensor_f_num
if sensor_cos_num <= 1:
h_cos_new = 0
h_dis_new = h_dis * sensor_cos_num
else:
h_cos_new = h_cos * sensor_cos_num # / 2
h_dis_new = h_dis * sensor_cos_num # / 2
cos_sq = h_cos_new / (h_cos_new + h_dis_new + h_f_new)
dis_sq = h_dis_new / (h_cos_new + h_dis_new + h_f_new)
f_sq = h_f_new / (h_cos_new + h_dis_new + h_f_new)
if sensor_cos_num == 2 and cos_sq > 0.2:
cos_sq = 0.2
dis_sq = 0.8 * h_dis_new / (h_dis_new + h_f_new)
f_sq = 0.8 * h_f_new / (h_dis_new + h_f_new)
"""if sensor_cos_num == 1:
if dis_sq / f_sq > sensor_cos_num/sensor_f_num:
dis_sq = sensor_cos_num/sensor_f_num
f_sq=1-dis_sq"""
# if h_dis_new/h_f_new > sensor_cos_num/sensor_f_num
return cos_sq, dis_sq, f_sq
def cal_sq_single_array(similarity_pre, if_direct):
if similarity_pre.max() - similarity_pre.min() == 0:
similarity_pre = np.ones(similarity_pre.shape)
else:
if if_direct == 1:
similarity_pre = (
0.998
* (similarity_pre - similarity_pre.min())
/ (similarity_pre.max() - similarity_pre.min())
+ 0.002
)
else:
similarity_pre = (
0.998
* (similarity_pre.max() - similarity_pre)
/ (similarity_pre.max() - similarity_pre.min())
+ 0.002
)
# calculate pij
similarity_p = similarity_pre / similarity_pre.sum()
# cal xinxishang
similarity_lnp = np.zeros((len(similarity_pre), 1))
for j in range(len(similarity_p)):
similarity_lnp[j] = -similarity_p[j] * math.log(similarity_p[j], math.e)
h = 1 - 1 / math.log(len(similarity_pre), math.e) * similarity_lnp.sum()
return similarity_pre, h
def cal_similarity_all_multi_new_sq_improve_double_lzr(
candidate_pipe,
similarity_mode,
pressure_leak,
monitor_p,
predict_p,
normal_p,
if_flow,
if_only_cos,
if_only_flow,
flow_leak,
monitor_f,
predict_f,
normal_f,
timestep_list,
Top_sensor_num,
if_gy,
effective_sensor,
cos_h,
dis_h,
dis_f_h,
if_compalsive,
max_flow,
):
similarity = pd.Series(dtype=float, index=candidate_pipe)
similarity_detail: pd.DataFrame | None = None
important_p_sensor = cal_top_sensors(monitor_p, predict_p, Top_sensor_num)
# important_f_sensor, basic_f = cal_top_f_sensor(normal_f)
important_f_sensor = monitor_f.columns
if (
len(important_p_sensor) > 0 or len(important_f_sensor) > 0
): # if len(important_p_sensor) > 0
break_flag = 0
pressure_leak_new = pressure_leak.swaplevel()
# flow_leak_new = flow_leak.swaplevel()
if isinstance(flow_leak, pd.DataFrame) and len(flow_leak) > 0:
flow_leak_new = flow_leak.swaplevel()
else:
flow_leak_new = None
total_similarity_cos = pd.DataFrame(index=timestep_list, columns=candidate_pipe)
total_similarity_dis = pd.DataFrame(index=timestep_list, columns=candidate_pipe)
total_similarity_dis_f = pd.DataFrame(
index=timestep_list, columns=candidate_pipe
)
for timestep in timestep_list:
# cal p_cos, p_dis, f_dis
if if_only_flow != 1:
pressure_leak_temp = pressure_leak_new.loc[timestep].loc[
:, effective_sensor
]
monitor_p_temp = monitor_p.loc[timestep, effective_sensor]
predict_p_temp = predict_p.loc[timestep, effective_sensor]
normal_p_temp = normal_p.loc[timestep, effective_sensor]
(
total_similarity_cos.loc[timestep, :],
total_similarity_dis.loc[timestep, :],
) = cal_similarity_all_cos_dis(
candidate_pipe,
pressure_leak_temp,
similarity_mode,
monitor_p_temp,
predict_p_temp,
normal_p_temp,
pressure_leak_new.loc[timestep].loc[:, monitor_p.columns],
monitor_p.loc[timestep, :],
predict_p.loc[timestep, :],
normal_p.loc[timestep, :],
important_p_sensor,
if_gy,
cos_or_flow=1,
)
if if_flow == 1:
if len(timestep_list) == 1:
leak_f_temp = flow_leak_new.loc[timestep].loc[:, important_f_sensor]
monitor_f_temp = monitor_f.loc[timestep, important_f_sensor]
predict_f_temp = predict_f.loc[timestep, important_f_sensor]
normal_f_temp = normal_f.loc[timestep, important_f_sensor]
basic_normal_f_temp = abs(max_flow.loc[important_f_sensor])
leak_f_temp = leak_f_temp / basic_normal_f_temp
monitor_f_temp = monitor_f_temp / basic_normal_f_temp
predict_f_temp = predict_f_temp / basic_normal_f_temp
normal_f_temp = normal_f_temp / basic_normal_f_temp
else:
basic_f = abs(max_flow.loc[important_f_sensor])
leak_f_temp = (
flow_leak_new.loc[timestep].loc[:, important_f_sensor] / basic_f
)
monitor_f_temp = (
monitor_f.loc[timestep, important_f_sensor] / basic_f
)
predict_f_temp = (
predict_f.loc[timestep, important_f_sensor] / basic_f
)
normal_f_temp = normal_f.loc[timestep, important_f_sensor] / basic_f
_, total_similarity_dis_f.loc[timestep, :] = cal_similarity_all_cos_dis(
candidate_pipe,
leak_f_temp,
similarity_mode,
monitor_f_temp,
predict_f_temp,
normal_f_temp,
flow_leak_new.loc[timestep].loc[:, monitor_f.columns],
monitor_f.loc[timestep, :],
predict_f.loc[timestep, :],
normal_f.loc[timestep, :],
important_f_sensor,
if_gy,
cos_or_flow=2,
)
else:
total_similarity_dis_f = []
similarity_all, cos_h, dis_h, dis_f_h = cal_sq_all_multi(
total_similarity_cos,
total_similarity_dis,
total_similarity_dis_f,
candidate_pipe,
timestep_list,
if_flow,
if_only_cos,
if_only_flow,
cos_h,
dis_h,
dis_f_h,
if_compalsive,
len(important_p_sensor),
len(important_f_sensor),
)
if len(timestep_list) == 1:
similarity = similarity_all.iloc[0]
elif len(timestep_list) > 3:
for each_candidate in candidate_pipe:
similarity[each_candidate] = remove_3_sigma(
similarity_all.loc[:, each_candidate]
)
else:
for each_candidate in candidate_pipe:
similarity[each_candidate] = similarity_all.loc[
:, each_candidate
].mean()
similarity = similarity.sort_values(ascending=False, kind="mergesort")
detail_index = [str(pipe) for pipe in candidate_pipe]
similarity_detail = pd.DataFrame(index=detail_index)
similarity_detail.index.name = "pipe_id"
if isinstance(total_similarity_cos, pd.DataFrame) and len(total_similarity_cos) > 0:
pressure_cos_mean = (
total_similarity_cos.mean(axis=0)
.reindex(candidate_pipe)
.to_numpy(dtype=float)
)
else:
pressure_cos_mean = np.full(len(candidate_pipe), np.nan)
if isinstance(total_similarity_dis, pd.DataFrame) and len(total_similarity_dis) > 0:
pressure_dis_mean = (
total_similarity_dis.mean(axis=0)
.reindex(candidate_pipe)
.to_numpy(dtype=float)
)
else:
pressure_dis_mean = np.full(len(candidate_pipe), np.nan)
if isinstance(total_similarity_dis_f, pd.DataFrame) and len(total_similarity_dis_f) > 0:
flow_dis_mean = (
total_similarity_dis_f.mean(axis=0)
.reindex(candidate_pipe)
.to_numpy(dtype=float)
)
else:
flow_dis_mean = np.full(len(candidate_pipe), np.nan)
similarity_detail["pressure_cos_mean"] = pressure_cos_mean
similarity_detail["pressure_dis_mean"] = pressure_dis_mean
similarity_detail["flow_dis_mean"] = flow_dis_mean
similarity_detail["weight_cos"] = float(cos_h)
similarity_detail["weight_dis"] = float(dis_h)
similarity_detail["weight_flow"] = float(dis_f_h)
similarity_detail["final_similarity"] = (
similarity.reindex(candidate_pipe).to_numpy(dtype=float)
)
similarity_detail["similarity_rank"] = (
similarity_detail["final_similarity"].rank(method="dense", ascending=False)
).astype(int)
similarity_detail["pressure_sensor_count"] = int(len(important_p_sensor))
similarity_detail["flow_sensor_count"] = int(len(important_f_sensor))
similarity_detail = similarity_detail.sort_values(
by="final_similarity", ascending=False, kind="mergesort"
)
else:
break_flag = 1
similarity = 0
cos_h = 0
dis_h = 0
dis_f_h = 0
return similarity, cos_h, dis_h, dis_f_h, break_flag, similarity_detail
def cal_similarity_all_cos_dis(
candidate_pipe,
pressure_leak,
similarity_mode,
monitor_p,
predict_p,
normal_p,
pressure_leak_all,
monitor_p_all,
predict_p_all,
normal_p_all,
important_sensor,
if_gy,
cos_or_flow,
):
similarity_cos = pd.Series(dtype=float, index=candidate_pipe)
similarity_dis = pd.Series(dtype=float, index=candidate_pipe)
dpressure = normal_p - pressure_leak
# 无用 ----------------------------------------------
mean_dpressure = dpressure.mean()
monitor_new = pd.DataFrame(index=["monitor"], columns=monitor_p.index)
monitor_new.iloc[0] = monitor_p
add_m_leak_pressure = [pressure_leak, monitor_p]
add_m_leak_pressure = pd.concat(add_m_leak_pressure)
pressure_leak_std = add_m_leak_pressure.std(axis=0, ddof=1)
pressure_leak_std = pd.Series(pressure_leak_std, index=pressure_leak.columns)
add_m_leak_pressure_all = [pressure_leak_all, monitor_p_all]
add_m_leak_pressure_all = pd.concat(add_m_leak_pressure_all)
pressure_leak_std_all = add_m_leak_pressure_all.std(axis=0, ddof=1)
pressure_leak_std_all = pd.Series(
pressure_leak_std_all, index=pressure_leak.columns
)
# 无用 ----------------------------------------------
monitor_p_temp = monitor_p
predict_p_temp = predict_p
normal_p_temp = normal_p
monitor_p_temp_all = monitor_p_all
predict_p_temp_all = predict_p_all
normal_p_temp_all = normal_p_all
record_success_candidate = []
record_success_no_candidate = []
for i in range(len(candidate_pipe)):
leak_p = pressure_leak.iloc[i, :]
leak_p_all = pressure_leak_all.iloc[i, :]
similarity_cos.iloc[i], similarity_dis.iloc[i], none_flag = (
cal_similarity_simple_return_dd(
similarity_mode,
monitor_p_temp,
predict_p_temp,
normal_p_temp,
leak_p,
monitor_p_temp_all,
predict_p_temp_all,
normal_p_temp_all,
leak_p_all,
important_sensor,
mean_dpressure,
pressure_leak_std,
pressure_leak_std_all,
if_gy,
cos_or_flow,
)
)
if none_flag == 0:
record_success_candidate.append(candidate_pipe[i])
else:
record_success_no_candidate.append(candidate_pipe[i])
similarity_cos, similarity_dis = adjust(
similarity_cos,
similarity_dis,
record_success_candidate,
record_success_no_candidate,
)
return similarity_cos, similarity_dis
def cal_top_f_sensor(normal_f):
if type(normal_f) == pd.core.frame.DataFrame:
mean_f = normal_f.mean()
else:
mean_f = normal_f
output_sensor = []
output_normal_f = pd.Series(dtype=object)
for i in range(len(mean_f.index)):
if abs(mean_f.iloc[i]) > 0.01 / 3600:
output_sensor.append(mean_f.index[i])
output_normal_f[mean_f.index[i]] = mean_f.iloc[i]
return output_sensor, output_normal_f
def cal_top_sensors(monitor_p, predict_p, Top_sensor_num):
dpressure = abs(predict_p - monitor_p)
if type(dpressure) == pd.core.frame.DataFrame:
dpressure = dpressure.mean()
dpressure_rank = dpressure.sort_values(ascending=False, kind="mergesort")
return list(dpressure_rank.index[:Top_sensor_num])
def remove_3_sigma(similarity_t):
all_sample = len(similarity_t.index)
apart_sample = math.ceil(all_sample * 0.6)
similarity = similarity_t.astype("float")
mean_t = similarity.mean()
std_t = similarity.std()
new_similarity = similarity[
(similarity <= mean_t + 3 * std_t) & (similarity >= mean_t - 3 * std_t)
]
mean_t_new = new_similarity.mean()
return mean_t_new
def update_similarity(leak_candidate_center, similarity, leak_center_dict):
similarity_new = pd.Series(dtype=float)
for each_center in leak_candidate_center:
houxuan_center = leak_center_dict[each_center]
if len(houxuan_center) > 1:
temp_similarity = similarity[houxuan_center]
similarity_new[each_center] = temp_similarity.max()
else:
if type(similarity[each_center]) == pd.core.series.Series:
similarity_new[each_center] = similarity[each_center].mean()
else:
similarity_new[each_center] = similarity[each_center]
similarity_new = similarity_new.sort_values(ascending=False, kind="mergesort")
return similarity_new
def extra_judge(
similarity, min_candidates_to_prune: int = 200, std_relax_factor: float = 0.5
):
if len(similarity.index) == 0:
return 1.0, similarity
if len(similarity.index) < int(min_candidates_to_prune):
return 1.0, similarity
mean_similarity = float(similarity.mean())
std_similarity = float(similarity.std())
if not math.isfinite(std_similarity):
std_similarity = 0.0
threshold = mean_similarity - float(std_relax_factor) * std_similarity
out_put_similarity = similarity[similarity >= threshold - 1e-10]
if len(out_put_similarity.index) == 0:
out_put_similarity = similarity.iloc[:1]
cut_ratio = len(out_put_similarity.index) / len(similarity.index)
return cut_ratio, out_put_similarity
def adjust_ratio(similarity_mode, cos_h, dis_h, dis_f_h, low_limit=0.1):
if similarity_mode == "CAF":
if cos_h < low_limit:
cos_h = low_limit
dis_f_h = 1 - cos_h
elif dis_f_h < low_limit:
dis_f_h = low_limit
cos_h = 1 - dis_f_h
elif similarity_mode == "CAD_new_gy":
if dis_h < low_limit:
dis_h = low_limit
cos_h = 1 - dis_h
elif cos_h < low_limit:
cos_h = low_limit
dis_h = 1 - cos_h
elif similarity_mode == "CDF":
normal_index = [0, 1, 2]
h_list = [cos_h, dis_h, dis_f_h]
if cos_h < low_limit:
h_list[0] = low_limit
normal_index.remove(0)
if dis_h < low_limit:
h_list[1] = low_limit
normal_index.remove(1)
if dis_f_h < low_limit:
h_list[2] = low_limit
normal_index.remove(2)
if len(normal_index) == 1:
h_list[normal_index[0]] = h_list[normal_index[0]] - (sum(h_list) - 1)
elif len(normal_index) == 2:
sum_list = sum(h_list)
multiper = 1 - (sum_list - 1) / (
h_list[normal_index[0]] + h_list[normal_index[1]]
)
h_list[normal_index[0]] = h_list[normal_index[0]] * multiper
h_list[normal_index[1]] = h_list[normal_index[1]] * multiper
cos_h, dis_h, dis_f_h = h_list[0], h_list[1], h_list[2]
return cos_h, dis_h, dis_f_h
# 返回相似性计算的模式(不同权重),是否计算流量相似性,是否只计算cos相似性,是否只计算流量相似性。
def decode_mode(similarity_mode):
if similarity_mode == "COS":
if_flow = 0
if_only_cos = 1
if_only_flow = 0
elif similarity_mode == "CAD_new_gy":
if_flow = 0
if_only_cos = 0
if_only_flow = 0
elif similarity_mode == "CDF":
if_flow = 1
if_only_cos = 0
if_only_flow = 0
elif similarity_mode == "CAF":
if_flow = 1
if_only_cos = 1
if_only_flow = 0
elif similarity_mode == "DIS":
if_flow = 1
if_only_cos = 2
if_only_flow = 0
elif similarity_mode == "OF":
if_flow = 1
if_only_cos = 0
if_only_flow = 1
return if_flow, if_only_cos, if_only_flow
@@ -0,0 +1,197 @@
"""噪声生成模块。"""
import copy
import random
import numpy as np
import pandas as pd
from .leak_signature import simple_add_leak, simple_recover_wn, simple_simulation_pf
def add_noise_pd(data, noise_type, noise_para):
output_data = copy.deepcopy(data)
if type(output_data) == pd.core.frame.Series:
if noise_type == "uni":
for x in output_data.index:
noise = (np.random.random() - 0.5) * 2
output_data[x] = output_data[x] + noise * noise_para
elif noise_type == "gauss":
noise = np.random.normal(loc=0, scale=noise_para, size=output_data.shape)
output_data = output_data + noise
elif type(output_data) == pd.core.frame.DataFrame:
if noise_type == "uni":
noise = (np.random.random(size=output_data.shape) - 0.5) * 2
output_data = output_data + noise * noise_para
elif noise_type == "gauss":
noise = np.random.normal(loc=0, scale=noise_para, size=output_data.shape)
output_data = output_data + noise
return output_data
def add_noise_number(data, noise_type, noise_para):
output_data = copy.deepcopy(data)
if noise_type == "uni":
noise = (np.random.random() - 0.5) * 2
output_data = output_data + noise * noise_para
elif noise_type == "gauss":
noise = random.gauss(0, noise_para)
output_data = output_data + noise
return output_data
def add_noise_number_flow(data, noise_para_mean, noise_para_std1, noise_para_std2):
output_data = copy.deepcopy(data)
noise_flag1 = np.random.random() - 0.5
if noise_flag1 < 0:
noise = noise_para_mean - abs(np.random.normal(loc=0, scale=noise_para_std1))
else:
noise = noise_para_mean + abs(np.random.normal(loc=0, scale=noise_para_std2))
noise_flag2 = np.random.random() - 0.5
if noise_flag2 < 0:
noise_f = noise * (-1)
else:
noise_f = noise
output_data = output_data + noise_f
return output_data
def produce_noise_number(noise_type, noise_para):
if noise_type == "uni":
noise = (np.random.random() - 0.5) * 2
noise = noise * noise_para
elif noise_type == "gauss":
noise = random.gauss(0, noise_para)
else:
noise = 0
return noise
def add_noise_percentage_pd(data, noise_type, noise_para):
output_data = copy.deepcopy(data)
if type(output_data) == pd.core.frame.Series:
if noise_type == "uni":
for x in output_data.index:
noise = (np.random.random() - 0.5) * 2
output_data[x] = output_data[x] * (1 + noise * noise_para / 100)
elif noise_type == "gauss":
for x in output_data.index:
noise = np.random.gauss(0, noise_para)
output_data[x] = output_data[x] * (1 + noise / 100)
# std_noise = noise.std()
elif type(output_data) == pd.core.frame.DataFrame:
if noise_type == "uni":
noise = (np.random.random(size=output_data.shape) - 0.5) * 2
output_data = output_data * (1 + noise * noise_para / 100)
elif noise_type == "gauss":
noise = np.random.normal(loc=0, scale=noise_para, size=output_data.shape)
output_data = output_data * (1 + noise / 100)
# std_noise = noise.std().mean()
return output_data
def add_noise_in_wn_pf(
wn,
pipe_c_noise,
timestep_list,
pipe_coefficient,
sensor_name,
sensor_f_name,
all_node,
basic_demand_pd,
noise_type,
noise_para,
leak_pipe,
leak_flow,
):
wn.options.time.duration = 0
pipe_roughness_change = add_noise_pd(pipe_coefficient, noise_type, pipe_c_noise)
wn = change_para_of_wn(wn, pipe_roughness_change)
record_pressure = pd.DataFrame(index=timestep_list, columns=sensor_name)
record_flow = pd.DataFrame(index=timestep_list, columns=sensor_f_name)
record_noise_all = pd.DataFrame(
index=pd.MultiIndex.from_product([timestep_list, all_node]),
columns=basic_demand_pd.columns,
)
record_noise_all = record_noise_all.sort_index()
# normal 获取添加噪声后的监测点数据
for i in range(len(timestep_list)):
wn, record_noise = change_node_demand(
wn, basic_demand_pd, all_node, noise_type, noise_para
)
record_noise_all.loc[timestep_list[i]].loc[:, :] = record_noise
pressure_temp, flow_temp = simple_simulation_pf(
wn, sensor_name, sensor_f_name, [], []
)
record_pressure.iloc[i, :] = pressure_temp
record_flow.iloc[i, :] = flow_temp
# leak_simulation 获取添加漏损后的监测点数据
record_pressure_leak = pd.DataFrame(index=timestep_list, columns=sensor_name)
record_flow_leak = pd.DataFrame(index=timestep_list, columns=sensor_f_name)
# 改_wz_________________________________________
# add leak
wn, whole_inf, add_pipe1 = simple_add_leak(wn, leak_flow, leak_pipe)
# simulation
for i in range(len(timestep_list)):
record_noise = record_noise_all.loc[timestep_list[i]]
wn = change_node_demand_leak(wn, record_noise, all_node)
pressure_temp, flow_temp = simple_simulation_pf(
wn, sensor_name, sensor_f_name, leak_pipe, add_pipe1
)
record_pressure_leak.iloc[i, :] = pressure_temp
record_flow_leak.iloc[i, :] = flow_temp
# delete leak
wn = simple_recover_wn(wn, whole_inf)
return wn, record_pressure, record_flow, record_pressure_leak, record_flow_leak
def change_node_demand(wn, basic_demand_pd, all_node, noise_type, noise_para):
# 改_wz_____________________________________
record_noise = pd.DataFrame(index=all_node, columns=basic_demand_pd.columns)
for each_node in all_node:
node = wn.get_node(each_node)
num_columns = len(basic_demand_pd.columns)
# 处理前N-1列(如果有)
for i in range(num_columns - 1):
# 获取原始值并添加噪声
record_noise.loc[each_node].iloc[i] = (
1 + produce_noise_number(noise_type, noise_para)
) * basic_demand_pd.loc[each_node].iloc[i]
node.demand_timeseries_list[i].base_value = record_noise.loc[
each_node
].iloc[i]
# 处理最后一列(当列数>=1时)
if num_columns >= 1:
last_col = basic_demand_pd.columns[-1]
original_last = basic_demand_pd.loc[each_node, last_col]
record_noise.loc[each_node, last_col] = original_last
node.demand_timeseries_list[-1].base_value = original_last
return wn, record_noise
def change_node_demand_leak(wn, record_noise, all_node):
sample_node = wn.get_node(all_node[0])
# num_categories = len(sample_node.demand_timeseries_list)
num_categories = 1
for each in all_node:
node = wn.get_node(each)
for i in range(num_categories):
node.demand_timeseries_list[i].base_value = record_noise.loc[each].iloc[i]
return wn
def change_para_of_wn(wn, pipe_roughness_change):
for pipe_name, pipe in wn.pipes():
pipe.roughness = pipe_roughness_change[pipe_name]
return wn
@@ -0,0 +1,137 @@
"""管网模型读取与图构建模块。"""
import copy
import numpy as np
import networkx as nx
import pandas as pd
import wntr
def load_inp(inp_name, inp_location, inp_time, driven_mode, require_p, minimum_p):
inp_file = inp_location + inp_name
wn = wntr.network.WaterNetworkModel(inp_file)
if driven_mode == "PDD":
wn.options.hydraulic.demand_model = "PDD"
wn.options.hydraulic.required_pressure = require_p
wn.options.hydraulic.minimum_pressure = minimum_p
else:
wn.options.hydraulic.demand_model = "DD"
return wn
def read_inf_inp(wn):
all_node = wn.node_name_list
node_elevation = wn.query_node_attribute("elevation")
node_coordinates = wn.query_node_attribute("coordinates")
all_pipe = wn.pipe_name_list
# 改_wz__________________________________
n_pipe = []
for p in all_pipe:
pipe = wn.get_link(p)
if pipe.initial_status == 0: # 状态为'Closed'
n_pipe.append(p)
candidate_pipe_init = sorted(set(all_pipe) - set(n_pipe))
pipe_start_node = wn.query_link_attribute(
"start_node_name", link_type=wntr.network.model.Pipe
)
pipe_end_node = wn.query_link_attribute(
"end_node_name", link_type=wntr.network.model.Pipe
)
pipe_length = wn.query_link_attribute("length")
pipe_diameter = wn.query_link_attribute("diameter")
return (
all_node,
node_elevation,
node_coordinates,
candidate_pipe_init,
pipe_start_node,
pipe_end_node,
pipe_length,
pipe_diameter,
)
def read_inf_inp_other(wn):
all_link = wn.link_name_list
pipe_start_node_all = wn.query_link_attribute("start_node_name")
pipe_end_node_all = wn.query_link_attribute("end_node_name")
return all_link, pipe_start_node_all, pipe_end_node_all
def construct_graph(wn):
length = wn.query_link_attribute("length")
G = wn.get_graph(wn, link_weight=length)
# 转为无向图
G0 = G.to_undirected()
# A0 = np.array(nx.adjacency_graph(G0).todense())
return G0 # , A0
def cal_pipe_coordinate(all_pipe, pipe_start_node, pipe_end_node, node_coordinates):
pipe_num = len(all_pipe)
pipe_coordinates = np.zeros([pipe_num, 2])
pipe_x = copy.deepcopy(pipe_start_node)
pipe_y = copy.deepcopy(pipe_start_node)
for i in range(pipe_num):
temp_pipe = all_pipe[i]
pipe_x[temp_pipe] = (
node_coordinates[pipe_start_node[temp_pipe]][0]
+ node_coordinates[pipe_end_node[temp_pipe]][0]
) / 2
pipe_y[temp_pipe] = (
node_coordinates[pipe_start_node[temp_pipe]][1]
+ node_coordinates[pipe_end_node[temp_pipe]][1]
) / 2
return pipe_x, pipe_y
def cal_node_coordinate(all_node, node_coordinates):
node_x = copy.deepcopy(node_coordinates)
node_y = copy.deepcopy(node_coordinates)
for i in range(len(node_x)):
temp_node = all_node[i]
node_x[temp_node] = node_coordinates[temp_node][0]
node_y[temp_node] = node_coordinates[temp_node][1]
return node_x, node_y
def produce_pattern_value(wn, all_node):
wn_o = copy.deepcopy(wn)
# 改_wz_____________________________
# sample_node = wn_o.get_node(all_node[0])
# num_categories = len(sample_node.demand_timeseries_list)
num_categories = 1
columns = [f"D{i}" for i in range(num_categories)]
basic_demand_pd = pd.DataFrame(index=all_node, columns=columns)
for each in all_node:
node = wn_o.get_node(each)
for i in range(num_categories):
basic_demand_pd.loc[each, columns[i]] = node.demand_timeseries_list[
i
].base_value
return basic_demand_pd
def _build_node_pipe_maps(
all_nodes, candidate_pipes, pipe_start_node, pipe_end_node, pipe_length
):
node_pipe_dic = {node: [] for node in all_nodes}
couple_node_length = {}
for pipe in candidate_pipes:
start_node = pipe_start_node[pipe]
end_node = pipe_end_node[pipe]
if start_node in node_pipe_dic:
node_pipe_dic[start_node].append(pipe)
if end_node in node_pipe_dic:
node_pipe_dic[end_node].append(pipe)
length = float(pipe_length[pipe])
couple_node_length[f"{start_node},{end_node}"] = length
couple_node_length[f"{end_node},{start_node}"] = length
return node_pipe_dic, couple_node_length
@@ -0,0 +1,456 @@
"""管网分区模块。"""
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