diff --git a/gui.py b/gui.py index f84a08a..6e641ce 100644 --- a/gui.py +++ b/gui.py @@ -91,6 +91,7 @@ def index(): "current_file_container": None, # 替换 label 为 container "info_container": None, # 新增信息展示容器 "ga_switch": None, # 遗传算法开关 + "mip_switch": None, # MIP开关 } def update_info_panel(): @@ -677,8 +678,9 @@ def index(): refs["log_box"].clear() log_queue = queue.Queue() - # 获取遗传算法开关状态 + # 获取开关状态 use_ga = refs["ga_switch"].value if refs["ga_switch"] else False + use_mip = refs["mip_switch"].value if refs["mip_switch"] else False class QueueLogger(io.StringIO): def write(self, message): @@ -728,6 +730,7 @@ def index(): interactive=False, plot_results=False, use_ga=use_ga, + use_mip=use_mip, ) # 在后台线程运行计算任务 @@ -920,8 +923,6 @@ def index(): # with refs["current_file_container"]: # ui.label("未选择文件").classes("text-xs text-gray-500 italic ml-1") - - # 3. 运行按钮 refs["run_btn"] = ( ui.button( @@ -937,6 +938,12 @@ def index(): "color=orange" ) + # 5. MIP开关 + with ui.column().classes("flex-1 gap-0 justify-center items-center"): + refs["mip_switch"] = ui.switch("启用MIP", value=False).props( + "color=blue" + ) + with ui.column().classes("w-full gap-4"): # 新增:信息展示卡片 with ( diff --git a/main.py b/main.py index e34ffbb..31c756f 100644 --- a/main.py +++ b/main.py @@ -15,6 +15,11 @@ from sklearn.cluster import KMeans from esau_williams import design_with_esau_williams from ga import design_with_ga +try: + from mip import design_with_mip +except ImportError: + design_with_mip = None + # 设置matplotlib支持中文显示 plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "Arial"] plt.rcParams["axes.unicode_minus"] = False @@ -1399,6 +1404,7 @@ def compare_design_methods( interactive=True, plot_results=True, use_ga=False, + use_mip=False, ): """ 比较MST和三种电缆方案下的K-means设计方法 @@ -1709,6 +1715,49 @@ def compare_design_methods( f" [GA] Cost: ¥{eval_ga['total_cost']:,.2f} | Loss: {eval_ga['total_loss']:.2f} kW | Circuits: {n_circuits_ga}" ) + if use_mip and design_with_mip: + # --- Run 5: Mixed Integer Programming --- + mip_name = f"{name} (MIP)" + conns_mip, turbines_mip = design_with_mip( + turbines.copy(), + substation, + current_specs, + voltage, + power_factor, + system_params, + evaluate_func=evaluate_design, + total_invest_func=total_investment, + get_max_capacity_func=get_max_cable_capacity_mw, + ) + eval_mip = evaluate_design( + turbines, + conns_mip, + substation, + cable_specs=current_specs, + is_offshore=is_offshore, + method_name=mip_name, + voltage=voltage, + power_factor=power_factor, + ) + n_circuits_mip = sum( + 1 + for d in eval_mip["details"] + if d["source"] == "substation" or d["target"] == "substation" + ) + comparison_results.append( + { + "name": mip_name, + "cost": eval_mip["total_cost"], + "loss": eval_mip["total_loss"], + "eval": eval_mip, + "turbines": turbines_mip, + "specs": current_specs, + } + ) + print( + f" [MIP] Cost: ¥{eval_mip['total_cost']:,.2f} | Loss: {eval_mip['total_loss']:.2f} kW | Circuits: {n_circuits_mip}" + ) + # 记录最佳 if eval_rot["total_cost"] < best_cost: best_cost = eval_rot["total_cost"] diff --git a/mip.py b/mip.py new file mode 100644 index 0000000..e887ed1 --- /dev/null +++ b/mip.py @@ -0,0 +1,142 @@ +import numpy as np +import pandas as pd +from scipy.spatial import distance_matrix +from scipy.sparse.csgraph import minimum_spanning_tree +from collections import defaultdict +import pulp + + +def design_with_mip( + turbines, + substation, + cable_specs=None, + voltage=66000, + power_factor=0.95, + system_params=None, + max_clusters=None, + time_limit=300, # seconds + evaluate_func=None, + total_invest_func=None, + get_max_capacity_func=None, +): + """ + 使用混合整数规划(MIP)优化集电线路布局 + :param turbines: 风机DataFrame + :param substation: 升压站坐标 + :param cable_specs: 电缆规格 + :param system_params: 系统参数(用于NPV计算) + :param max_clusters: 最大簇数,默认基于功率计算 + :param time_limit: 求解时间限制(秒) + :param evaluate_func: 评估函数 + :param total_invest_func: 总投资计算函数 + :param get_max_capacity_func: 获取最大容量函数 + :return: 连接列表和带有簇信息的turbines + """ + if get_max_capacity_func: + max_mw = get_max_capacity_func(cable_specs, voltage, power_factor) + else: + max_mw = 100.0 # 默认值 + + total_power = turbines["power"].sum() + if max_clusters is None: + max_clusters = int(np.ceil(total_power / max_mw)) + n_turbines = len(turbines) + + # 预计算距离矩阵 + all_coords = np.vstack([substation, turbines[["x", "y"]].values]) + dist_matrix_full = distance_matrix(all_coords, all_coords) + + # MIP 模型 + prob = pulp.LpProblem("WindFarmCollectorMIP", pulp.LpMinimize) + + # 决策变量:风机分配到簇 (binary) + x = pulp.LpVariable.dicts( + "assign", (range(n_turbines), range(max_clusters)), cat="Binary" + ) + + # 簇使用变量 (binary) + y = pulp.LpVariable.dicts("use_cluster", range(max_clusters), cat="Binary") + + # 目标函数:最小化总成本 (简化版:距离成本) + # 这里使用简化成本:簇内距离 + 到升压站距离 + prob += pulp.lpSum( + [ + dist_matrix_full[i + 1, j + 1] * x[i][k] * x[j][k] + for i in range(n_turbines) + for j in range(n_turbines) + for k in range(max_clusters) + if i < j + ] + ) + pulp.lpSum( + [ + dist_matrix_full[0, i + 1] * y[k] # 假设每个簇连接到升压站 + for i in range(n_turbines) + for k in range(max_clusters) + ] + ) + + # 约束:每个风机分配到一个簇 + for i in range(n_turbines): + prob += pulp.lpSum([x[i][k] for k in range(max_clusters)]) == 1 + + # 簇功率约束 + for k in range(max_clusters): + prob += ( + pulp.lpSum([turbines.iloc[i]["power"] * x[i][k] for i in range(n_turbines)]) + <= max_mw * y[k] + ) + + # 如果簇未使用,则无分配 + for k in range(max_clusters): + for i in range(n_turbines): + prob += x[i][k] <= y[k] + + # 求解 + solver = pulp.PULP_CBC_CMD(timeLimit=time_limit) + status = prob.solve(solver) + + if pulp.LpStatus[prob.status] != "Optimal": + print(f"MIP not optimal: {pulp.LpStatus[prob.status]}") + # 返回默认方案,如 MST + from main import design_with_mst + + return design_with_mst(turbines, substation) + + # 提取结果 + cluster_assign = [-1] * n_turbines + for i in range(n_turbines): + for k in range(max_clusters): + if pulp.value(x[i][k]) > 0.5: + cluster_assign[i] = k + break + + # 构建连接 + clusters = defaultdict(list) + for i, c in enumerate(cluster_assign): + clusters[c].append(i) + + connections = [] + for c, members in clusters.items(): + if len(members) == 0: + continue + coords = turbines.iloc[members][["x", "y"]].values + if len(members) > 1: + dm = distance_matrix(coords, coords) + mst = minimum_spanning_tree(dm).toarray() + for i in range(len(members)): + for j in range(len(members)): + if mst[i, j] > 0: + connections.append( + ( + f"turbine_{members[i]}", + f"turbine_{members[j]}", + mst[i, j], + ) + ) + # 连接到升压站 + dists = [dist_matrix_full[0, m + 1] for m in members] + closest = members[np.argmin(dists)] + connections.append((f"turbine_{closest}", "substation", min(dists))) + + turbines["cluster"] = cluster_assign + return connections, turbines