"""漏损模拟模块。""" 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] ]