优化传感器布置算法,修复数据库更新逻辑

This commit is contained in:
2026-04-17 17:21:50 +08:00
parent bf2aaa5ff7
commit 3b712ea467
7 changed files with 795 additions and 291 deletions
+92 -44
View File
@@ -20,6 +20,7 @@ import geopandas as gpd
from sklearn.metrics import pairwise_distances
import app.services.project_info as project_info
# 2025/03/12
# Step1: 获取节点坐标
def getCoor(wn: wntr.network.WaterNetworkModel) -> pandas.DataFrame:
@@ -31,7 +32,7 @@ def getCoor(wn: wntr.network.WaterNetworkModel) -> pandas.DataFrame:
# site: pandas.Series
# index:节点名称(wn.node_name_list
# values:每个节点的坐标,格式为 tuple(如 (x, y) 或 (x, y, z)
site = wn.query_node_attribute('coordinates')
site = wn.query_node_attribute("coordinates")
# Coor: pandas.Series
# index:与site相同(节点名称)。
# values:坐标转换为numpy.ndarray(如array([10.5, 20.3])
@@ -43,9 +44,9 @@ def getCoor(wn: wntr.network.WaterNetworkModel) -> pandas.DataFrame:
x.append(Coor.values[i][0]) # 将 x 坐标存入 x 列表。
y.append(Coor.values[i][1]) # 将 y 坐标存入 y 列表
# xy: dict[str, list], x、y 坐标的字典
xy = {'x': x, 'y': y}
xy = {"x": x, "y": y}
# Coor_node: pandas.DataFrame, 存储节点 x, y 坐标的 DataFrame
Coor_node = pd.DataFrame(xy, index=wn.node_name_list, columns=['x', 'y'])
Coor_node = pd.DataFrame(xy, index=wn.node_name_list, columns=["x", "y"])
return Coor_node
@@ -87,23 +88,23 @@ def skater_partition(G, n_clusters):
字典形式的聚类结果,键为区域编号,值为该区域内的节点列表。
"""
# 1. 获取所有节点坐标,假设每个节点都有 'pos' 属性
pos = nx.get_node_attributes(G, 'pos')
pos = nx.get_node_attributes(G, "pos")
nodes = list(G.nodes())
# 构造坐标数组:每行为 [x, y]
coords = np.array([pos[node] for node in nodes])
# 2. 构造 GeoDataFrame:创建 DataFrame 并生成 geometry 列
df = pd.DataFrame(coords, columns=['x', 'y'], index=nodes)
df = pd.DataFrame(coords, columns=["x", "y"], index=nodes)
# 利用 shapely 的 Point 构造空间位置
df['geometry'] = df.apply(lambda row: Point(row['x'], row['y']), axis=1)
gdf = gpd.GeoDataFrame(df, geometry='geometry')
df["geometry"] = df.apply(lambda row: Point(row["x"], row["y"]), axis=1)
gdf = gpd.GeoDataFrame(df, geometry="geometry")
# 3. 构造空间权重矩阵,使用 4 近邻方法(k=4,可根据实际情况调整)
w = ps.weights.KNN.from_array(coords, k=4)
w.transform = 'R'
w.transform = "R"
# 4. 调用 SKATER:新版本 API 要求传入 gdf, w 以及 attrs_name(这里使用 'x' 和 'y' 作为属性)
skater = Skater(gdf, w, attrs_name=['x', 'y'], n_clusters=n_clusters)
skater = Skater(gdf, w, attrs_name=["x", "y"], n_clusters=n_clusters)
skater.solve()
# 5. 获取聚类标签,构造成字典格式
@@ -133,24 +134,24 @@ def spectral_partition(G, n_clusters):
键为聚类标签,值为该聚类对应的节点列表。
"""
# 1. 获取节点空间坐标,注意保证每个节点都有 'pos' 属性
pos_dict = nx.get_node_attributes(G, 'pos')
pos_dict = nx.get_node_attributes(G, "pos")
nodes = list(G.nodes())
coords = np.array([pos_dict[node] for node in nodes])
# 2. 计算节点之间的欧氏距离矩阵
D = pairwise_distances(coords, metric='euclidean')
D = pairwise_distances(coords, metric="euclidean")
# 3. 计算 sigma 值:这里取所有距离的均值,当然也可以根据实际情况调整
sigma = np.mean(D)
# 4. 构造相似度矩阵:使用高斯核函数
# A(i, j) = exp( -d(i,j)^2 / (2*sigma^2) )
A = np.exp(- (D ** 2) / (2 * sigma ** 2))
A = np.exp(-(D**2) / (2 * sigma**2))
# 5. 使用谱聚类进行图分区
clustering = SpectralClustering(n_clusters=n_clusters,
affinity='precomputed',
random_state=0)
clustering = SpectralClustering(
n_clusters=n_clusters, affinity="precomputed", random_state=0
)
labels = clustering.fit_predict(A)
# 6. 构造字典形式的分区结果
@@ -160,6 +161,7 @@ def spectral_partition(G, n_clusters):
return groups
# 2025/03/12
# Step3: wn_func类,水力计算
# wn_func 主要用于计算:
@@ -181,7 +183,7 @@ class wn_func(object):
self.results = wntr.sim.EpanetSimulator(wn).run_sim() # 存储运行结果
self.wn = wn
# self.qpandas.DataFrame,管道流量,索引为时间步长,列为管道名称
self.q = self.results.link['flowrate']
self.q = self.results.link["flowrate"]
# ReservoirIndex / Tankindex: list[str],水库 / 水箱节点名称列表
ReservoirIndex = wn.reservoir_name_list
Tankindex = wn.tank_name_list
@@ -191,7 +193,7 @@ class wn_func(object):
# self.nodes: list[str],所有节点的名称
self.nodes = wn.node_name_list
# self.coordinatespandas.Series,节点坐标,索引为节点名,值为 (x, y) 坐标的 tuple
self.coordinates = wn.query_node_attribute('coordinates')
self.coordinates = wn.query_node_attribute("coordinates")
# allpumps / allvalves: list[str],所有泵/阀门名称列表
allpumps = wn.pump_name_list
allvalves = wn.valve_name_list
@@ -222,17 +224,27 @@ class wn_func(object):
# 泵的起终点、tank、reservoir
# self.delnodes: list[str],需要删除的节点(包括水库、泵、阀门连接的节点)
self.delnodes = list(
set(ReservoirIndex).union(Tankindex, pumpstnode, pumpednode, valvestnode, valveednode, Reservoirednode))
set(ReservoirIndex).union(
Tankindex,
pumpstnode,
pumpednode,
valvestnode,
valveednode,
Reservoirednode,
)
)
# 泵、起终点为tank、reservoir的管道
# self.delpipes: list[str],需要删除的管道(包括水库、泵、阀门连接的管道)
self.delpipes = list(set(wn.pump_name_list).union(wn.valve_name_list).union(Reservoirpipe))
self.delpipes = list(
set(wn.pump_name_list).union(wn.valve_name_list).union(Reservoirpipe)
)
self.pipes = [pipe for pipe in wn.pipe_name_list if pipe not in self.delpipes]
# self.L: list[float],所有管道的长度(以米为单位)
self.L = wn.query_link_attribute('length')[self.pipes].tolist()
self.L = wn.query_link_attribute("length")[self.pipes].tolist()
self.n = len(self.nodes)
self.m = len(self.pipes)
# self.unit_headloss: list[float],单位水头损失(headloss 数据的第一行,单位:米/km)
self.unit_headloss = self.results.link['headloss'].iloc[0, :].tolist()
self.unit_headloss = self.results.link["headloss"].iloc[0, :].tolist()
##
self.delnodes1 = list(set(ReservoirIndex).union(Tankindex))
@@ -245,7 +257,9 @@ class wn_func(object):
end_node = wn.links[pipe].end_node.name
self.less_than_min_diameter_junction_list.extend([start_node, end_node])
# 去重
self.less_than_min_diameter_junction_list = list(set(self.less_than_min_diameter_junction_list))
self.less_than_min_diameter_junction_list = list(
set(self.less_than_min_diameter_junction_list)
)
# Step3.2: 计算水力距离
def CtoS(self):
@@ -266,7 +280,7 @@ class wn_func(object):
q = self.q
L = self.L
# H1pandas.DataFrame,水头数据,索引为时间步长,列为节点名
H1 = self.results.node['head'].T
H1 = self.results.node["head"].T
# hhlist[float],计算管道两端水头之差
hh = []
# 水头损失
@@ -280,8 +294,18 @@ class wn_func(object):
# headlosspandas.DataFrame,管道水头损失矩阵
headloss = pd.DataFrame(hh, index=pipes).T
# s1:管道阻力系数,s2:将管道阻力系数与管道的起始节点和终止节点对应
hf = pd.DataFrame(np.array([0] * (n ** 2)).reshape(n, n), index=nodes, columns=nodes, dtype=float)
weightL = pd.DataFrame(np.array([0] * (n ** 2)).reshape(n, n), index=nodes, columns=nodes, dtype=float)
hf = pd.DataFrame(
np.array([0] * (n**2)).reshape(n, n),
index=nodes,
columns=nodes,
dtype=float,
)
weightL = pd.DataFrame(
np.array([0] * (n**2)).reshape(n, n),
index=nodes,
columns=nodes,
dtype=float,
)
# s2为对应管道起始节点与终止节点的粗糙度系数矩阵,index代表起始节点,columns代表终止节点
G = nx.DiGraph()
for i in range(0, m):
@@ -298,11 +322,16 @@ class wn_func(object):
weightL.loc[b, a] = headloss.loc[0, pipe] * L[i]
G.add_weighted_edges_from([(b, a, weightL.loc[b, a])])
hydraulicL = pd.DataFrame(np.array([0] * (n ** 2)).reshape(n, n), index=nodes, columns=nodes, dtype=float)
hydraulicL = pd.DataFrame(
np.array([0] * (n**2)).reshape(n, n),
index=nodes,
columns=nodes,
dtype=float,
)
for a in nodes:
if a in G.nodes:
d = nx.shortest_path_length(G, source=a, weight='weight')
d = nx.shortest_path_length(G, source=a, weight="weight")
for b in list(d.keys()):
hydraulicL.loc[a, b] = d[b]
@@ -331,11 +360,17 @@ class wn_func(object):
for t in self.wn.tanks():
self.nonjunc_index.append(t[0])
# Connnumpy.matrix,节点-管道连接矩阵,起点 -1,终点 1
Conn = np.mat(np.zeros([n, m - p - v])) # 节点和管道的关系矩阵,行为节点,列为管道,起点为-1,终点为1
Conn = np.mat(
np.zeros([n, m - p - v])
) # 节点和管道的关系矩阵,行为节点,列为管道,起点为-1,终点为1
# NConnnumpy.matrix,节点-节点连接矩阵,有管道相连的地方设为 1
NConn = np.mat(np.zeros([n, n])) # 节点之间的关系,之间有管道为1,反之为0
# pipeslist[str],去除泵和阀门的管道列表
pipes = [pipe for pipe in self.wn.pipes() if pipe not in self.wn.pumps() and pipe not in self.wn.valves()]
pipes = [
pipe
for pipe in self.wn.pipes()
if pipe not in self.wn.pumps() and pipe not in self.wn.valves()
]
for pipe_name, pipe in pipes:
start = self.wn.node_name_list.index(pipe.start_node_name)
end = self.wn.node_name_list.index(pipe.end_node_name)
@@ -345,12 +380,21 @@ class wn_func(object):
NConn[start, end] = 1
NConn[end, start] = 1
self.A = Conn
link_name_list = [link for link in self.wn.link_name_list if
link not in self.wn.pump_name_list and link not in self.wn.valve_name_list]
self.A2 = pd.DataFrame(self.A, index=self.wn.node_name_list, columns=link_name_list)
link_name_list = [
link
for link in self.wn.link_name_list
if link not in self.wn.pump_name_list
and link not in self.wn.valve_name_list
]
self.A2 = pd.DataFrame(
self.A, index=self.wn.node_name_list, columns=link_name_list
)
self.A2 = self.A2.drop(self.delnodes)
for pipe in self.delpipes:
if pipe not in self.wn.pump_name_list and pipe not in self.wn.valve_name_list:
if (
pipe not in self.wn.pump_name_list
and pipe not in self.wn.valve_name_list
):
self.A2 = self.A2.drop(columns=pipe)
self.junc_list = self.A2.index
self.A2 = np.mat(self.A2) # 节点管道关系
@@ -372,10 +416,10 @@ class wn_func(object):
except EpanetException:
pass
finally:
h = result.link['headloss'][self.pipes].values[0]
q = result.link['flowrate'][self.pipes].values[0]
l = self.wn.query_link_attribute('length')[self.pipes]
C = self.wn.query_link_attribute('roughness')[self.pipes]
h = result.link["headloss"][self.pipes].values[0]
q = result.link["flowrate"][self.pipes].values[0]
l = self.wn.query_link_attribute("length")[self.pipes]
C = self.wn.query_link_attribute("roughness")[self.pipes]
# headlossnumpy.ndarray,水头损失数组
headloss = np.array(h)
# 调整流量方向
@@ -393,7 +437,7 @@ class wn_func(object):
try:
det = np.linalg.det(X)
except RuntimeError as e:
sign, logdet = slogdet(X) # 防止溢出
sign, logdet = slogdet(X) # 防止溢出
det = sign * np.exp(logdet)
if det != 0:
J_H_Cw = X.I * A * S
@@ -430,7 +474,10 @@ class Sensorplacement(wn_func):
"""
Sensorplacement 类继承了 wn_func 类,并且用于计算和优化传感器布置的位置。
"""
def __init__(self, wn: wntr.network.WaterNetworkModel, sensornum: int, min_diameter: int):
def __init__(
self, wn: wntr.network.WaterNetworkModel, sensornum: int, min_diameter: int
):
"""
:param wn: 由wntr生成的模型
@@ -442,7 +489,9 @@ class Sensorplacement(wn_func):
# 1.某个节点到所有节点的加权距离之和
# 2.某个节点到该组内所有节点的加权距离之和
def sensor(self, SS: pandas.DataFrame, G: networkx.Graph, group: dict[int, list[str]]):
def sensor(
self, SS: pandas.DataFrame, G: networkx.Graph, group: dict[int, list[str]]
):
"""
sensor 方法是用来根据灵敏度矩阵 SS 和加权图 G 来确定传感器布置位置的
:param SS: 灵敏度矩阵,每个节点的行和列代表不同节点,矩阵元素表示节点间的灵敏度。SS.iloc[i, :] 表示第 i 行对应节点 i 到所有其他节点的灵敏度
@@ -527,7 +576,7 @@ def get_ID(name: str, sensor_num: int, min_diameter: int) -> list[str]:
:return: 测压点节点ID
"""
# inp_file_realstr,输入文件名,表示原始水力模型文件的路径,该文件格式为 EPANET 输入文件(.inp),包含管网的结构信息、节点、管道、泵等数据
inp_file_real = f'./db_inp/{name}.db.inp'
inp_file_real = f"./db_inp/{name}.db.inp"
# sensornumint,需要布置的传感器数量
# sensornum = sensor_num
# wn_realwntr.network.WaterNetworkModel,加载 EPANET 水力模型
@@ -538,7 +587,7 @@ def get_ID(name: str, sensor_num: int, min_diameter: int) -> list[str]:
results_real = sim_real.run_sim()
# real_Clist[float],包含所有管道粗糙度的列表
real_C = wn_real.query_link_attribute('roughness').tolist()
real_C = wn_real.query_link_attribute("roughness").tolist()
# wn_fun1wn_func(继承自 object),创建 wn_func 类的实例,传入 wn_real 水力模型对象。wn_func 用于计算管网相关的水力属性,比如水力距离、灵敏度等
wn_fun1 = wn_func(wn_real, min_diameter=min_diameter)
# nodeslist[str],管网的节点名称列表
@@ -598,7 +647,6 @@ def get_ID(name: str, sensor_num: int, min_diameter: int) -> list[str]:
sensorindex, sensorindex_2 = wn_fun.sensor(SS, G, group) # 初始的sensorindex
# print(str(sensor_num), "个测压点,测压点位置:", sensorindex)
# 重新打开数据库
# if is_project_open(name=name):
# close_project(name=name)
@@ -637,7 +685,7 @@ def get_ID(name: str, sensor_num: int, min_diameter: int) -> list[str]:
return sensorindex
if __name__ == '__main__':
if __name__ == "__main__":
sensorindex = get_ID(name=project_info.name, sensor_num=20, min_diameter=300)
print(sensorindex)