# 改进灵敏度法
import networkx
import numpy as np
import pandas
import wntr
import pandas as pd
import copy
import matplotlib.pyplot as plt
import networkx as nx
from sklearn.cluster import KMeans
from wntr.epanet.toolkit import EpanetException
from numpy.linalg import slogdet
import random
from matplotlib.lines import Line2D
from sklearn.cluster import SpectralClustering
import libpysal as ps
from spopt.region import Skater
from shapely.geometry import Point
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:
    """
    获取管网模型的节点坐标
    :param wn: 由wntr生成的模型
    :return: 节点坐标
    """
    # site: pandas.Series
    #   index：节点名称（wn.node_name_list）
    #   values：每个节点的坐标，格式为 tuple（如 (x, y) 或 (x, y, z)）
    site = wn.query_node_attribute('coordinates')
    # Coor: pandas.Series
    #   index：与site相同（节点名称）。
    #   values：坐标转换为numpy.ndarray（如array([10.5, 20.3])）
    Coor = site.apply(lambda x: np.array(x))  # 将节点坐标转换为numpy数组
    # x, y: list[float]
    x = []  # 存储所有节点的 x 坐标
    y = []  # 存储所有节点的 y 坐标
    for i in range(0, len(Coor)):
        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}
    # Coor_node: pandas.DataFrame, 存储节点 x, y 坐标的 DataFrame
    Coor_node = pd.DataFrame(xy, index=wn.node_name_list, columns=['x', 'y'])
    return Coor_node


# 2025/03/12
# Step2: KMeans 聚类
# 将节点用kmeans根据坐标分为k组，存入字典g
def kgroup(coor: pandas.DataFrame, knum: int) -> dict[int, list[str]]:
    """
    使用KMeans聚类，将节点坐标分组
    :param coor: 存储所有节点的坐标数据
    :param knum: 需要分成的聚类数
    :return: 聚类结果字典
    """
    g = {}
    # estimator: sklearn.cluster.KMeans,KMeans 聚类模型
    estimator = KMeans(n_clusters=knum)
    estimator.fit(coor)
    # label_pred: numpy.ndarray（int）,每个点的类别标签
    label_pred = estimator.labels_
    for i in range(0, knum):
        g[i] = coor[label_pred == i].index.tolist()
    return g


def skater_partition(G, n_clusters):
    """
    使用 SKATER 算法对输入的无向图 G 进行区域划分，
    保证每个划分区域在图论意义上是连通的，
    同时依据节点坐标的空间信息进行划分。

    参数:
        G: networkx.Graph
            带有节点坐标属性（键为 'pos'）的无向图。
        n_clusters: int
            希望划分的区域数量。

    返回:
        groups: dict
            字典形式的聚类结果，键为区域编号，值为该区域内的节点列表。
    """
    # 1. 获取所有节点坐标，假设每个节点都有 '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)
    # 利用 shapely 的 Point 构造空间位置
    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'

    # 4. 调用 SKATER：新版本 API 要求传入 gdf, w 以及 attrs_name（这里使用 'x' 和 'y' 作为属性）
    skater = Skater(gdf, w, attrs_name=['x', 'y'], n_clusters=n_clusters)
    skater.solve()

    # 5. 获取聚类标签，构造成字典格式
    labels = skater.labels_
    groups = {}
    for label, node in zip(labels, nodes):
        groups.setdefault(label, []).append(node)

    return groups


def spectral_partition(G, n_clusters):
    """
    利用谱聚类算法对图 G 进行分区：
      1. 根据所有节点的空间坐标计算欧氏距离矩阵；
      2. 利用高斯核函数构造相似度矩阵；
      3. 使用 SpectralClustering 进行归一化割，返回分区结果。

    参数:
        G: networkx.Graph
            每个节点需要有 'pos' 属性，其值为 (x, y) 坐标。
        n_clusters: int
            希望划分的聚类数目。

    返回:
        groups: dict
            键为聚类标签，值为该聚类对应的节点列表。
    """
    # 1. 获取节点空间坐标，注意保证每个节点都有 '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')

    # 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))

    # 5. 使用谱聚类进行图分区
    clustering = SpectralClustering(n_clusters=n_clusters,
                                    affinity='precomputed',
                                    random_state=0)
    labels = clustering.fit_predict(A)

    # 6. 构造字典形式的分区结果
    groups = {}
    for label, node in zip(labels, nodes):
        groups.setdefault(label, []).append(node)

    return groups

# 2025/03/12
# Step3: wn_func类，水力计算
# wn_func 主要用于计算：
#     水力距离（hydraulic length）：即节点之间的水力阻力。
#     灵敏度分析（sensitivity analysis）：用于优化测压点的布置。
# 一些与水力相关的函数，包括 CtoS：求水力距离,stafun：求状态函数F
# # diff：求F对P的导数，返回灵敏度矩阵A
# # sensitivity：返回灵敏度和总灵敏度
class wn_func(object):

    # Step3.1: 初始化
    def __init__(self, wn: wntr.network.WaterNetworkModel, min_diameter: int):
        """
        获取管网模型信息
        :param wn: 由wntr生成的模型
        :param min_diameter: 安装的最小管径
        """
        # self.results: wntr.sim.results.SimulationResults,仿真结果，包含压力、流量、水头等数据
        self.results = wntr.sim.EpanetSimulator(wn).run_sim()  # 存储运行结果
        self.wn = wn
        # self.q：pandas.DataFrame,管道流量，索引为时间步长，列为管道名称
        self.q = self.results.link['flowrate']
        # ReservoirIndex / Tankindex: list[str],水库 / 水箱节点名称列表
        ReservoirIndex = wn.reservoir_name_list
        Tankindex = wn.tank_name_list
        # 删除水库节点，删除与直接水库相连的虚拟管道
        # self.pipes: list[str],所有管道的名称
        self.pipes = wn.pipe_name_list
        # self.nodes: list[str],所有节点的名称
        self.nodes = wn.node_name_list
        # self.coordinates：pandas.Series,节点坐标，索引为节点名，值为 (x, y) 坐标的 tuple
        self.coordinates = wn.query_node_attribute('coordinates')
        # allpumps / allvalves: list[str],所有泵/阀门名称列表
        allpumps = wn.pump_name_list
        allvalves = wn.valve_name_list
        # pumpstnode / pumpednode / valvestnode / valveednode: list[str],存储泵和阀门 起终点节点的名称
        pumpstnode = []
        pumpednode = []
        valvestnode = []
        valveednode = []
        # Reservoirpipe / Reservoirednode: list[str],记录与水库相关的管道和节点
        Reservoirpipe = []
        Reservoirednode = []
        for pump in allpumps:
            pumpstnode.append(wn.links[pump].start_node.name)
            pumpednode.append(wn.links[pump].end_node.name)
        for valve in allvalves:
            valvestnode.append(wn.links[valve].start_node.name)
            valveednode.append(wn.links[valve].end_node.name)
        for pipe in self.pipes:
            if wn.links[pipe].start_node.name in ReservoirIndex:
                Reservoirpipe.append(pipe)
                Reservoirednode.append(wn.links[pipe].end_node.name)
            if wn.links[pipe].start_node.name in Tankindex:
                Reservoirpipe.append(pipe)
                Reservoirednode.append(wn.links[pipe].end_node.name)
            if wn.links[pipe].end_node.name in Tankindex:
                Reservoirpipe.append(pipe)
                Reservoirednode.append(wn.links[pipe].start_node.name)
        # 泵的起终点、tank、reservoir
        # self.delnodes: list[str],需要删除的节点（包括水库、泵、阀门连接的节点）
        self.delnodes = list(
            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.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.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.delnodes1 = list(set(ReservoirIndex).union(Tankindex))

        # === 改动新增部分：筛选管径小于 min_diameter 的管道节点 ===
        self.less_than_min_diameter_junction_list = []
        for pipe in self.pipes:
            diameter = wn.links[pipe].diameter
            if diameter < min_diameter:
                start_node = wn.links[pipe].start_node.name
                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))

    # Step3.2: 计算水力距离
    def CtoS(self):
        """
        计算水力距离矩阵
        :return:
        """
        # 水力距离：当行索引对应的节点为控制点时，列索引对应的节点距离控制点的（路径*水头损失）的最小值
        # nodes：list[str]（节点名称）
        nodes = copy.deepcopy(self.nodes)
        # pipes：list[str]（管道名称）
        pipes = self.pipes
        wn = self.wn
        # n / m：int（节点数 / 管道数）
        n = self.n
        m = self.m
        s1 = [0] * m
        q = self.q
        L = self.L
        # H1：pandas.DataFrame,水头数据，索引为时间步长，列为节点名
        H1 = self.results.node['head'].T
        # hh：list[float],计算管道两端水头之差
        hh = []
        # 水头损失
        for p in pipes:
            h1 = self.wn.links[p].start_node.name
            h1 = H1.loc[str(h1)]
            h2 = self.wn.links[p].end_node.name
            h2 = H1.loc[str(h2)]
            hh.append(abs(h1 - h2))
        hh = np.array(hh)
        # headloss：pandas.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)
        # s2为对应管道起始节点与终止节点的粗糙度系数矩阵，index代表起始节点，columns代表终止节点
        G = nx.DiGraph()
        for i in range(0, m):
            pipe = pipes[i]
            a = wn.links[pipe].start_node.name
            b = wn.links[pipe].end_node.name
            if q.loc[0, pipe] > 0:
                hf.loc[a, b] = headloss.loc[0, pipe]
                weightL.loc[a, b] = headloss.loc[0, pipe] * L[i]
                G.add_weighted_edges_from([(a, b, weightL.loc[a, b])])

            else:
                hf.loc[b, a] = headloss.loc[0, pipe]
                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)

        for a in nodes:
            if a in G.nodes:
                d = nx.shortest_path_length(G, source=a, weight='weight')
                for b in list(d.keys()):
                    hydraulicL.loc[a, b] = d[b]

        hydraulicL = hydraulicL.drop(self.delnodes)
        hydraulicL = hydraulicL.drop(self.delnodes, axis=1)

        # 求加权水力距离
        return hydraulicL, G

    # Step3.3: 计算灵敏度矩阵
    # 获取关系矩阵
    def get_Conn(self):
        """
        计算管网连接关系矩阵
        :return:
        """
        m = self.wn.num_links
        n = self.wn.num_nodes
        p = self.wn.num_pumps
        v = self.wn.num_valves

        self.nonjunc_index = []
        self.non_link_index = []
        for r in self.wn.reservoirs():
            self.nonjunc_index.append(r[0])
        for t in self.wn.tanks():
            self.nonjunc_index.append(t[0])
        # Conn：numpy.matrix，节点-管道连接矩阵，起点 -1，终点 1
        Conn = np.mat(np.zeros([n, m - p - v]))  # 节点和管道的关系矩阵，行为节点，列为管道，起点为-1，终点为1
        # NConn：numpy.matrix，节点-节点连接矩阵，有管道相连的地方设为 1
        NConn = np.mat(np.zeros([n, n]))  # 节点之间的关系，之间有管道为1，反之为0
        # pipes：list[str]，去除泵和阀门的管道列表
        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)
            p_index = self.wn.link_name_list.index(pipe_name)
            Conn[start, p_index] = -1
            Conn[end, p_index] = 1
            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)
        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:
                self.A2 = self.A2.drop(columns=pipe)
        self.junc_list = self.A2.index
        self.A2 = np.mat(self.A2)  # 节点管道关系
        self.A3 = NConn

    def Jaco(self, hL: pandas.DataFrame):
        """
        计算灵敏度矩阵（节点压力对粗糙度变化的响应）
        :param hL: 水力距离矩阵
        :return:
        """
        # global result
        # A：numpy.matrix, 节点-管道关系矩阵
        A = self.A2
        wn = self.wn

        try:
            result = wntr.sim.EpanetSimulator(wn).run_sim()
        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]
            # headloss：numpy.ndarray,水头损失数组
            headloss = np.array(h)
            # 调整流量方向
            for i in range(0, len(q)):
                if q[i] < 0:
                    A[:, i] = -A[:, i]
            # q：numpy.ndarray,流量数组
            q = np.abs(q)
            # 两个灵敏度矩阵
            # B / S：numpy.matrix,灵敏度计算的中间矩阵
            B = np.mat(np.diag(q / ((1.852 * headloss) + 1e-10)))
            S = np.mat(np.diag(q / C))
            # X：numpy.matrix, 灵敏度矩阵
            X = A * B * A.T
            try:
                det = np.linalg.det(X)
            except RuntimeError as e:
                sign, logdet = slogdet(X)   # 防止溢出
                det = sign * np.exp(logdet)
            if det != 0:
                J_H_Cw = X.I * A * S
                # J_H_Q = -X.I
                J_q_Cw = S - B * A.T * X.I * A * S  # 去掉了delnodes和delpipes
                # J_q_Q = B * A.T * X.I
            else:  # 当X不可逆
                J_H_Cw = np.linalg.pinv(X) @ A @ S
                # J_H_Q = -np.linalg.pinv(X)
                J_q_Cw = S - B * A.T * np.linalg.pinv(X) * A * S
                # J_q_Q = B * A.T * np.linalg.pinv(X)

            Sen_pressure = []
            S_pressure = np.abs(J_H_Cw).sum(axis=1).tolist()  # 修改为绝对值
            for ss in S_pressure:
                Sen_pressure.append(ss[0])
            # 求总灵敏度
            SS_pressure = copy.deepcopy(hL)
            for i in range(0, len(Sen_pressure)):
                SS_pressure.iloc[i, :] = SS_pressure.iloc[i, :] * Sen_pressure[i]
            SS = copy.deepcopy(hL)
            for i in range(0, len(Sen_pressure)):
                SS.iloc[i, :] = SS.iloc[i, :] * Sen_pressure[i]
            # SS[i,j]:节点nodes[i]的灵敏度*该节点到nodes[j]的水力距离
            return SS


# 2025/03/12
# Step4: 传感器布置优化
# Sensorplacement
# weight：分配权重
# sensor：传感器布置的位置
class Sensorplacement(wn_func):
    """
    Sensorplacement 类继承了 wn_func 类，并且用于计算和优化传感器布置的位置。
    """
    def __init__(self, wn: wntr.network.WaterNetworkModel, sensornum: int, min_diameter: int):
        """

        :param wn: 由wntr生成的模型
        :param sensornum: 传感器的数量
        :param min_diameter: 安装的最小管径
        """
        wn_func.__init__(self, wn, min_diameter=min_diameter)
        self.sensornum = sensornum

    # 1.某个节点到所有节点的加权距离之和
    # 2.某个节点到该组内所有节点的加权距离之和
    def sensor(self, SS: pandas.DataFrame, G: networkx.Graph, group: dict[int, list[str]]):
        """
        sensor 方法是用来根据灵敏度矩阵 SS 和加权图 G 来确定传感器布置位置的
        :param SS: 灵敏度矩阵，每个节点的行和列代表不同节点，矩阵元素表示节点间的灵敏度。SS.iloc[i, :] 表示第 i 行对应节点 i 到所有其他节点的灵敏度
        :param G: 加权图，表示管网的拓扑结构，每个节点通过管道连接。图的边的权重通常是根据水力距离或者流量等计算的
        :param group: 节点分组，字典的键是分组编号，值是该组的节点名称列表
        :return:
        """
        # 传感器布置个数以及位置
        # W = self.weight()
        n = self.n - len(self.delnodes)
        nodes = copy.deepcopy(self.nodes)
        for node in self.delnodes:
            nodes.remove(node)
        # sumSS：list[float],每个节点到其他节点的灵敏度之和。SS.iloc[i, :] 返回第 i 个节点与所有其他节点的灵敏度值，sum(SS.iloc[i, :]) 计算这些灵敏度值的总和。
        sumSS = []
        for i in range(0, n):
            sumSS.append(sum(SS.iloc[i, :]))
        # 一个整数范围，表示每个节点的索引，用作sumSS_ DataFrame的索引
        indices = range(0, n)
        # sumSS_：pandas.DataFrame,将 sumSS 转换成 DataFrame 格式，并且将节点的总灵敏度保存到 CSV 文件 sumSS_data.csv 中
        sumSS_ = pd.DataFrame(np.array(sumSS), index=indices)
        # sumSS_.to_csv('sumSS_data.csv')  # 存储节点总灵敏度

        # sumSS：pandas.DataFrame,sumSS 被转换为 DataFrame 类型，并且按总灵敏度（即灵敏度之和）降序排列。此时，sumSS 是按节点的灵敏度之和排序的 DataFrame
        sumSS = pd.DataFrame(np.array(sumSS), index=nodes)
        sumSS = sumSS.sort_values(by=[0], ascending=[False])
        # sensorindex：list[str],用于存储根据灵敏度排序选出的传感器位置的节点名称,存储根据总灵敏度排序的节点列表，用于传感器布置
        sensorindex = []
        # sensorindex_2：list[str],用于存储每组内根据灵敏度排序选出的传感器位置的节点名称,存储每个组内根据灵敏度排序选择的传感器节点
        sensorindex_2 = []
        # group_S：dict[int, pandas.DataFrame],存储每个组内的灵敏度矩阵
        group_S = {}
        # group_sumSS：dict[int, list[float]],存储每个组内节点的总灵敏度，值为每个组内节点灵敏度之和的列表
        group_sumSS = {}

        # 改动
        for i in range(0, len(group)):
            for node in self.delnodes:
                # 这里的group[i]是每个组的节点列表，代码首先去除已经被标记为删除的节点self.delnodes
                if node in group[i]:
                    group[i].remove(node)
            group_S[i] = SS.loc[group[i], group[i]]
            # 对每个组内的节点，计算组内节点的总灵敏度（group_sumSS[i]）。它将每个组内节点的灵敏度值相加，并且按灵敏度降序排序
            group_sumSS[i] = []
            for j in range(0, len(group[i])):
                group_sumSS[i].append(sum(group_S[i].iloc[j, :]))
            group_sumSS[i] = pd.DataFrame(np.array(group_sumSS[i]), index=group[i])
            group_sumSS[i] = group_sumSS[i].sort_values(by=[0], ascending=[False])
            for node in self.less_than_min_diameter_junction_list:
                # 这里的group_sumSS[i]是每个分组的灵敏度节点排序列表，去除已经被标记为删除的节点self.less_than_min_diameter_junction_list
                if node in group_sumSS[i]:
                    group_sumSS[i].remove(node)
        pass

        # 1.选sumSS最大的节点，然后把这个节点所在的那个组删掉，就可以不再从这个组选点。再重新排序选sumSS最大的;
        # 2.在每组内选group_sumSS最大的节点
        # 在这个循环中，首先选择灵敏度最高的节点Smaxnode并添加到sensorindex。然后根据灵敏度排序，删除已选的节点并继续选择下一个灵敏度最大的节点。这个过程用于选择传感器的位置
        sensornum = self.sensornum
        for i in range(0, sensornum):
            # Smaxnode：str,最大灵敏度节点，sumSS.index[0] 表示灵敏度最高的节点
            Smaxnode = sumSS.index[0]
            sensorindex.append(Smaxnode)
            sensorindex_2.append(group_sumSS[i].index[0])

            for key, value in group.items():
                if Smaxnode in value:
                    sumSS = sumSS.drop(index=group[key])
                    continue

            sumSS = sumSS.sort_values(by=[0], ascending=[False])

        return sensorindex, sensorindex_2


# 2025/03/13
def get_ID(name: str, sensor_num: int, min_diameter: int) -> list[str]:
    """
    获取布置测压点的坐标,初始测压点布置根据灵敏度来布置，计算初始情况下的校准过程的error
    :param name: 数据库名称
    :param sensor_num: 测压点数目
    :param min_diameter: 安装的最小管径
    :return: 测压点节点ID
    """
    # inp_file_real：str,输入文件名，表示原始水力模型文件的路径,该文件格式为 EPANET 输入文件（.inp），包含管网的结构信息、节点、管道、泵等数据
    inp_file_real = f'./db_inp/{name}.db.inp'
    # sensornum：int,需要布置的传感器数量
    # sensornum = sensor_num
    # wn_real：wntr.network.WaterNetworkModel,加载 EPANET 水力模型
    wn_real = wntr.network.WaterNetworkModel(inp_file_real)  # 真实粗糙度的原始管网
    # sim_real：wntr.sim.EpanetSimulator,创建一个水力仿真器对象
    sim_real = wntr.sim.EpanetSimulator(wn_real)
    # results_real：wntr.sim.results.SimulationResults,运行仿真并返回结果
    results_real = sim_real.run_sim()

    # real_C：list[float]，包含所有管道粗糙度的列表
    real_C = wn_real.query_link_attribute('roughness').tolist()
    # wn_fun1：wn_func（继承自 object），创建 wn_func 类的实例，传入 wn_real 水力模型对象。wn_func 用于计算管网相关的水力属性，比如水力距离、灵敏度等
    wn_fun1 = wn_func(wn_real, min_diameter=min_diameter)
    # nodes：list[str]，管网的节点名称列表
    nodes = wn_fun1.nodes
    # delnodes：list[str]，被删除的节点（如水库、泵、阀门连接的节点等）
    delnodes = wn_fun1.delnodes
    # Coor_node：pandas.DataFrame
    Coor_node = getCoor(wn_real)
    Coor_node = Coor_node.drop(wn_fun1.delnodes)
    nodes = [node for node in wn_fun1.nodes if node not in delnodes]
    # coordinates：pandas.Series，存储所有节点的坐标，类型为 Series，索引为节点名称，值为 (x, y) 坐标对
    coordinates = wn_fun1.coordinates

    # 随机产生监测点
    # junctionnum：int，nodes 的长度，表示节点的数量
    junctionnum = len(nodes)
    # random_numbers：list[int]，使用 random.sample 随机选择 sensornum（20）个节点的编号。它返回一个不重复的随机编号列表
    # random_numbers = random.sample(range(junctionnum), sensor_num)
    # for i in range(sensor_num):
    #     # print(random_numbers[i])

    wn_fun1.get_Conn()
    # hL：pandas.DataFrame，水力距离矩阵，表示每个节点到其他节点的水力阻力
    # G：networkx.DiGraph，加权有向图，表示管网的拓扑结构，节点之间的边带有权重
    hL, G = wn_fun1.CtoS()
    # SS：pandas.DataFrame，灵敏度矩阵，表示每个节点对管网变化（如粗糙度、流量等）的响应
    SS = wn_fun1.Jaco(hL)
    # group：dict[int, list[str]]，使用 kgroup 函数将节点按坐标分成若干组，每组包含的节点数不一定相同。group 是一个字典，键为分组编号，值为节点名列表

    G1 = wn_real.to_graph()
    G1 = G1.to_undirected()  # 变为无向图

    group = kgroup(Coor_node, sensor_num)
    # group = skater_partition(G1, sensor_num)
    # group = spectral_partition(G1, sensor_num)

    # print(group)
    # --------------------- 保存 group 数据 ---------------------
    # 将 group 数据转换为一个“长格式”的 DataFrame，
    # 每一行记录一个节点及其所属的分组
    # group_data = []
    # for group_id, node_list in group.items():
    #     for node in node_list:
    #         group_data.append({"Group": group_id, "Node": node})
    #
    # df_group = pd.DataFrame(group_data)
    #
    # # 保存为 Excel 文件，文件名为 "group.xlsx"；index=False 表示不保存行索引
    # df_group.to_excel("group.xlsx", index=False)

    # wn_fun：Sensorplacement（继承自wn_func）
    # 创建Sensorplacement类的实例，传入水力网络模型wn_real和传感器数量sensornum。Sensorplacement用于计算和布置传感器
    wn_fun = Sensorplacement(wn_real, sensor_num, min_diameter=min_diameter)
    wn_fun.__dict__.update(wn_fun1.__dict__)
    # sensorindex：list[str]，初始传感器布置位置的节点名称
    # sensorindex_2：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)
    # open_project(name=name)
    # for node_id in sensorindex :
    #     sensor_coord[node_id] = get_node_coord(name=name, node_id=node_id)
    # close_project(name=name)
    # print(sensor_coord)
    # # 分区画图
    # colorlist = ['lightpink', 'coral', 'rosybrown', 'olive', 'powderblue', 'lightskyblue', 'steelblue', 'peachpuff','brown','silver','indigo','lime','gold','violet','maroon','navy','teal','magenta','cyan',
    #              'burlywood', 'tan', 'slategrey', 'thistle', 'lightseagreen', 'lightgreen', 'red','blue','yellow','orange','purple','grey','green','pink','lightblue','beige','chartreuse','turquoise','lavender','fuchsia','coral']
    # G = wn_real.to_graph()
    # G = G.to_undirected()  # 变为无向图
    # pos = nx.get_node_attributes(G, 'pos')
    # pass
    #
    # for i in range(0, sensor_num):
    #     ax = plt.gca()
    #     ax.set_title(inp_file_real + str(sensor_num))
    #     nodes = nx.draw_networkx_nodes(G, pos, nodelist=group[i], node_color=colorlist[i], node_size=10)
    # nodes = nx.draw_networkx_nodes(G, pos,
    #                                nodelist=sensorindex_2, node_color='red', node_size=70, node_shape='*'
    #                                )
    # edges = nx.draw_networkx_edges(G, pos)
    # ax.spines['top'].set_visible(False)
    # ax.spines['right'].set_visible(False)
    # ax.spines['bottom'].set_visible(False)
    # ax.spines['left'].set_visible(False)
    # plt.savefig(inp_file_real + str(sensor_num) + ".png", dpi=300)
    # plt.show()
    #
    # wntr.graphics.plot_network(wn_real, node_attribute=sensorindex_2, node_size=50, node_labels=False,
    #                            title=inp_file_real + '_Projetion' + str(sensor_num))
    # plt.savefig(inp_file_real + '_S' + str(sensor_num) + ".png", dpi=300)
    # plt.show()
    return sensorindex


if __name__ == '__main__':
    sensorindex = get_ID(name=project_info.name, sensor_num=20, min_diameter=300)

    print(sensorindex)
    # 将 sensor_coord 字典转换为 DataFrame，
    # 使用 orient='index' 表示字典的键作为 DataFrame 的行索引，
    # 数据中每个键对应的 value 是一个子字典，其键 'x' 和 'y' 成为 DataFrame 的列名
    # df_sensor_coord = pd.DataFrame.from_dict(sensor_coord, orient='index')
    #
    # # 将索引名称设为 'Node'
    # df_sensor_coord.index.name = 'Node'
    #
    # # 保存到 Excel 文件
    # df_sensor_coord.to_excel("sensor_coord.xlsx", index=True)
