From b5718a0cc2a39531ca4741a2655b8379306e3853 Mon Sep 17 00:00:00 2001 From: dmy Date: Fri, 2 Jan 2026 00:25:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A3=8E=E7=94=B5=E5=9C=BA?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=96=B9=E6=A1=88=E5=AF=B9=E6=AF=94=E7=AE=97?= =?UTF-8?q?=E6=B3=95=EF=BC=9A=E6=B7=BB=E5=8A=A0=E6=97=8B=E8=BD=AC=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E5=92=8C=E5=8F=8C=E6=A8=A1=E5=BC=8F=E5=AF=B9=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增design_with_rotational_sweep函数:实现旋转优化算法 - 修改compare_design_methods函数: * 将MST结果纳入对比列表 * 每个电缆场景运行Base和Rotational两种算法 * 添加成本和损耗对比显示 * 优化可视化展示和文件输出 - 改进算法选择逻辑:增强簇数计算的智能化 - 更新输出格式:区分不同算法结果并优化显示 --- main.py | 258 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 192 insertions(+), 66 deletions(-) diff --git a/main.py b/main.py index 497ba86..484ded5 100644 --- a/main.py +++ b/main.py @@ -274,10 +274,10 @@ def design_with_kmeans(turbines, substation, n_clusters=3): return cluster_connections + substation_connections, turbines -# 3.5 带容量约束的扇区扫描算法 (Capacitated Angular Sweep) +# 3.5 带容量约束的扇区扫描算法 (Capacitated Sweep) - 基础版 def design_with_capacitated_sweep(turbines, substation, cable_specs=None): """ - 使用带容量约束的扇区扫描算法设计集电线路 + 使用带容量约束的扇区扫描算法设计集电线路 (基础版:单次扫描) 原理: 1. 计算所有风机相对于升压站的角度。 2. 找到角度间隔最大的位置作为起始“切割线”,以避免切断密集的风机群。 @@ -286,7 +286,7 @@ def design_with_capacitated_sweep(turbines, substation, cable_specs=None): """ # 1. 获取电缆最大容量 max_mw = get_max_cable_capacity_mw(cable_specs) - print(f"DEBUG: 扇区扫描算法启动 - 单回路容量限制: {max_mw:.2f} MW") + # print(f"DEBUG: 扇区扫描算法启动 - 单回路容量限制: {max_mw:.2f} MW") substation_coord = substation[0] @@ -311,18 +311,12 @@ def design_with_capacitated_sweep(turbines, substation, cable_specs=None): wrap_diff = (2 * np.pi) - (angles[-1] - angles[0]) diffs = np.append(diffs, wrap_diff) - # 找到最大间隙的索引 (该索引对应元素的 *后面* 是间隙) + # 找到最大间隙的索引 max_gap_idx = np.argmax(diffs) - # 如果最大间隙不是在队尾,则需要旋转数组,使最大间隙成为新的起点(即队尾) - # 实际上我们希望扫描的起点是最大间隙的“右侧” - # 例如: [0, 10, 100, 110]. Gaps: 10, 90, 10, 250. Max gap is wrap (250). Start at 0 is fine. - # 例如: [0, 10, 200, 210]. Gaps: 10, 190, 10, 150. Max gap is 190 (idx 1). - # 我们应该从 idx 2 (200) 开始扫描。 - + # 旋转数组,使最大间隙成为新的起点 start_idx = (max_gap_idx + 1) % n - # 重新排序风机列表 if start_idx != 0: work_df = pd.concat([work_df.iloc[start_idx:], work_df.iloc[:start_idx]]).reset_index(drop=True) @@ -335,10 +329,7 @@ def design_with_capacitated_sweep(turbines, substation, cable_specs=None): for i, row in work_df.iterrows(): p = row['power'] - # 检查是否超载 - # 注意:如果是空簇,必须加入至少一个(即使单个风机超载也得加,但在风电中单机不会超载) if len(current_indices) > 0 and (current_power + p > max_mw): - # 当前簇已满,结束当前簇,开启新簇 work_df.loc[current_indices, 'cluster'] = cluster_id cluster_id += 1 current_power = 0 @@ -347,22 +338,15 @@ def design_with_capacitated_sweep(turbines, substation, cable_specs=None): current_indices.append(i) current_power += p - # 处理最后一个簇 if current_indices: work_df.loc[current_indices, 'cluster'] = cluster_id - cluster_id += 1 # 计数用 + cluster_id += 1 - print(f"DEBUG: 生成了 {cluster_id} 个回路 (簇)") - - # 将 cluster 标记映射回原始 turbines DataFrame (通过 original_id 或 索引匹配) - # 这里我们简单地重建 turbines,因为 work_df 包含了所有信息且顺序变了 - # 为了保持外部一致性,我们把 cluster 列 map 回去 - # 建立 id -> cluster 的映射 id_to_cluster = dict(zip(work_df['id'], work_df['cluster'])) turbines['cluster'] = turbines['id'].map(id_to_cluster) - # 5. 对每个簇内部进行MST连接 (复用现有逻辑) + # 5. 对每个簇内部进行MST连接 cluster_connections = [] substation_connections = [] n_clusters = cluster_id @@ -372,7 +356,6 @@ def design_with_capacitated_sweep(turbines, substation, cable_specs=None): if len(cluster_turbines) == 0: continue - # --- 簇内 MST --- cluster_indices = cluster_turbines.index.tolist() coords = cluster_turbines[['x', 'y']].values @@ -387,18 +370,141 @@ def design_with_capacitated_sweep(turbines, substation, cable_specs=None): target = f'turbine_{cluster_indices[j]}' cluster_connections.append((source, target, mst[i, j])) - # --- 连接到升压站 --- - # 找到簇内离升压站最近的风机 + # 连接到升压站 dists = np.sqrt((cluster_turbines['x'] - substation_coord[0])**2 + (cluster_turbines['y'] - substation_coord[1])**2) - closest_id = dists.idxmin() # 返回的是原始DataFrame的index - - # 添加升压站连接 + closest_id = dists.idxmin() min_dist = dists.min() substation_connections.append((f'turbine_{closest_id}', 'substation', min_dist)) return cluster_connections + substation_connections, turbines +# 3.6 旋转扫描算法 (Rotational Sweep) - 优化版 +def design_with_rotational_sweep(turbines, substation, cable_specs=None): + """ + 使用带容量约束的扇区扫描算法设计集电线路 (优化版:旋转扫描) + 原理: + 1. 计算所有风机相对于升压站的角度并排序。 + 2. 遍历所有可能的起始角度(即尝试以每一台风机作为扫描的起点)。 + 3. 对每种起始角度,贪婪地将风机加入回路直到满载。 + 4. 对每种分组方案计算MST成本,选出总成本最低的方案。 + """ + # 1. 获取电缆最大容量 + max_mw = get_max_cable_capacity_mw(cable_specs) + # print(f"DEBUG: 扇区扫描算法启动 - 单回路容量限制: {max_mw:.2f} MW") + + substation_coord = substation[0] + + # 2. 计算角度 (使用 arctan2 返回 -pi 到 pi) + work_df = turbines.copy() + dx = work_df['x'] - substation_coord[0] + dy = work_df['y'] - substation_coord[1] + work_df['angle'] = np.arctan2(dy, dx) + + # 按角度排序 + work_df = work_df.sort_values('angle').reset_index(drop=True) + + n_turbines = len(work_df) + best_cost = float('inf') + best_connections = [] + best_turbines_state = None + best_start_idx = -1 + + # 遍历所有可能的起始点 + for start_idx in range(n_turbines): + + # 构建当前旋转顺序的风机列表 + if start_idx == 0: + current_df = work_df.copy() + else: + current_df = pd.concat([work_df.iloc[start_idx:], work_df.iloc[:start_idx]]).reset_index(drop=True) + + # --- 贪婪分组 --- + current_df['cluster'] = -1 + cluster_id = 0 + current_power = 0 + current_indices_in_df = [] + + powers = current_df['power'].values + + for i in range(n_turbines): + p = powers[i] + + if len(current_indices_in_df) > 0 and (current_power + p > max_mw): + current_df.loc[current_indices_in_df, 'cluster'] = cluster_id + cluster_id += 1 + current_power = 0 + current_indices_in_df = [] + + current_indices_in_df.append(i) + current_power += p + + if current_indices_in_df: + current_df.loc[current_indices_in_df, 'cluster'] = cluster_id + cluster_id += 1 + + # --- 计算该分组方案的成本 --- + current_total_length = 0 + + n_clusters = cluster_id + for cid in range(n_clusters): + cluster_rows = current_df[current_df['cluster'] == cid] + if len(cluster_rows) == 0: continue + + # 1. 簇内 MST 长度 + coords = cluster_rows[['x', 'y']].values + if len(cluster_rows) > 1: + dm = distance_matrix(coords, coords) + mst = minimum_spanning_tree(dm).toarray() + mst_len = mst.sum() + current_total_length += mst_len + + # 2. 连接升压站长度 + dists = np.sqrt((cluster_rows['x'] - substation_coord[0])**2 + + (cluster_rows['y'] - substation_coord[1])**2) + min_dist = dists.min() + current_total_length += min_dist + + # --- 比较并保存最佳结果 --- + if current_total_length < best_cost: + best_cost = current_total_length + best_start_idx = start_idx + best_id_to_cluster = dict(zip(current_df['id'], current_df['cluster'])) + + # --- 根据最佳方案重新生成详细连接 --- + turbines['cluster'] = turbines['id'].map(best_id_to_cluster) + + final_connections = [] + + unique_clusters = turbines['cluster'].unique() + unique_clusters = [c for c in unique_clusters if not pd.isna(c) and c >= 0] + + for cid in unique_clusters: + cluster_turbines = turbines[turbines['cluster'] == cid] + if len(cluster_turbines) == 0: continue + + cluster_indices = cluster_turbines.index.tolist() + coords = cluster_turbines[['x', 'y']].values + + if len(cluster_indices) > 1: + dist_matrix_local = distance_matrix(coords, coords) + mst = minimum_spanning_tree(dist_matrix_local).toarray() + + for i in range(len(cluster_indices)): + for j in range(len(cluster_indices)): + if mst[i, j] > 0: + source = f'turbine_{cluster_indices[i]}' + target = f'turbine_{cluster_indices[j]}' + final_connections.append((source, target, mst[i, j])) + + dists = np.sqrt((cluster_turbines['x'] - substation_coord[0])**2 + + (cluster_turbines['y'] - substation_coord[1])**2) + closest_idx_in_df = dists.idxmin() + min_dist = dists.min() + final_connections.append((f'turbine_{closest_idx_in_df}', 'substation', min_dist)) + + return final_connections, turbines + def get_max_cable_capacity_mw(cable_specs=None): """ 计算给定电缆规格中能够承载的最大功率 (单位: MW)。 @@ -962,12 +1068,22 @@ def compare_design_methods(excel_path=None, n_clusters_override=None): f"MST Method (Standard Cables)\nTotal Cost: ¥{mst_evaluation['total_cost']/10000:.2f}万", ax=axes[0]) - print(f"\n===== 开始比较电缆方案 (基于 Capacitated Sweep) =====") + print(f"\n===== 开始比较电缆方案 =====") best_cost = float('inf') best_result = None comparison_results = [] + + # 将 MST 结果也加入对比列表,方便查看 + comparison_results.append({ + 'name': 'MST Method', + 'cost': mst_evaluation['total_cost'], + 'loss': mst_evaluation['total_loss'], + 'eval': mst_evaluation, + 'turbines': turbines.copy(), # MST 不改变 turbines,但为了统一格式 + 'specs': specs_1 + }) for i, (name, current_specs) in enumerate(scenarios): print(f"\n--- {name} ---") @@ -978,71 +1094,81 @@ def compare_design_methods(excel_path=None, n_clusters_override=None): # 计算参数 total_power = turbines['power'].sum() max_cable_mw = get_max_cable_capacity_mw(cable_specs=current_specs) - n_cable_types = len(current_specs) - # 确定簇数 + # 确定簇数 (针对 Base 算法) if n_clusters_override is not None: n_clusters = n_clusters_override min_needed = int(np.ceil(total_power / max_cable_mw)) if n_clusters < min_needed: - print(f" Warning: 指定簇数 {n_clusters} 小于理论最小需求 {min_needed}。设计可能会失败或严重过载。") + print(f" Warning: 指定簇数 {n_clusters} 小于理论最小需求 {min_needed}。") else: - # 自动计算:取 (理论最小需求) 和 (基于电缆型号分级估算) 的较大值 min_needed = int(np.ceil(total_power / max_cable_mw)) - # 这里的逻辑是:如果电缆级差很密,可能不需要那么多回路;如果电缆很大,回路可以少。 - # 原有的逻辑是 len(turbines)/n_cable_types,这只是一个经验值。 - # 我们主要保证满足容量: - n_clusters = min_needed - # 稍微增加一点裕度,避免因为离散分布导致最后一点功率塞不进 - # 或者保持原有的启发式逻辑,取最大值 + n_cable_types = len(current_specs) heuristic = int(np.ceil(len(turbines) / n_cable_types)) n_clusters = max(min_needed, heuristic) if n_clusters > len(turbines): n_clusters = len(turbines) print(f" 最大电缆容量: {max_cable_mw:.2f} MW") - print(f" 设计回路数: {n_clusters}") - # 运行设计 - conns, clustered_turbines = design_with_capacitated_sweep( + # --- Run 1: Base Algorithm (Capacitated Sweep) --- + base_name = f"{name} (Base)" + conns_base, turbines_base = design_with_capacitated_sweep( turbines.copy(), substation, cable_specs=current_specs ) - - # 评估 - evaluation = evaluate_design( - turbines, conns, substation, cable_specs=current_specs, - is_offshore=is_offshore, method_name=name + eval_base = evaluate_design( + turbines, conns_base, substation, cable_specs=current_specs, + is_offshore=is_offshore, method_name=base_name ) - # 记录结果 comparison_results.append({ - 'name': name, - 'cost': evaluation['total_cost'], - 'loss': evaluation['total_loss'], - 'eval': evaluation, - 'turbines': clustered_turbines, + 'name': base_name, + 'cost': eval_base['total_cost'], + 'loss': eval_base['total_loss'], + 'eval': eval_base, + 'turbines': turbines_base, 'specs': current_specs }) + print(f" [Base] Cost: ¥{eval_base['total_cost']:,.2f} | Loss: {eval_base['total_loss']:.2f} kW") + + # --- Run 2: Rotational Algorithm (Optimization) --- + rot_name = f"{name} (Rotational)" + conns_rot, turbines_rot = design_with_rotational_sweep( + turbines.copy(), substation, cable_specs=current_specs + ) + eval_rot = evaluate_design( + turbines, conns_rot, substation, cable_specs=current_specs, + is_offshore=is_offshore, method_name=rot_name + ) - # 输出简报 - print(f" 总成本: ¥{evaluation['total_cost']:,.2f}") - print(f" 总损耗: {evaluation['total_loss']:.2f} kW") + comparison_results.append({ + 'name': rot_name, + 'cost': eval_rot['total_cost'], + 'loss': eval_rot['total_loss'], + 'eval': eval_rot, + 'turbines': turbines_rot, + 'specs': current_specs + }) + print(f" [Rotational] Cost: ¥{eval_rot['total_cost']:,.2f} | Loss: {eval_rot['total_loss']:.2f} kW") # 记录最佳 - if evaluation['total_cost'] < best_cost: - best_cost = evaluation['total_cost'] - best_result = comparison_results[-1] + if eval_rot['total_cost'] < best_cost: + best_cost = eval_rot['total_cost'] + # best_result 不再需要单独维护,最后遍历 comparison_results 即可 - # 可视化 (axes 1, 2, 3) + if eval_base['total_cost'] < best_cost: + best_cost = eval_base['total_cost'] + + # 可视化 (只画 Base 版本) ax_idx = i + 1 if ax_idx < 4: - n_actual_clusters = clustered_turbines['cluster'].nunique() - title = f"{name} ({n_actual_clusters} circuits)\nCost: ¥{evaluation['total_cost']/10000:.2f}万 | Loss: {evaluation['total_loss']:.2f} kW" - visualize_design(clustered_turbines, substation, evaluation['details'], title, ax=axes[ax_idx]) + n_circuits = turbines_base['cluster'].nunique() + title = f"{base_name} ({n_circuits} circuits)\nCost: ¥{eval_base['total_cost']/10000:.2f}万" + visualize_design(turbines_base, substation, eval_base['details'], title, ax=axes[ax_idx]) plt.tight_layout() output_filename = 'wind_farm_design_comparison.png' plt.savefig(output_filename, dpi=300) - print(f"\n比较图已保存至: {output_filename}") + print(f"\n比较图(Base版)已保存至: {output_filename}") # 准备文件路径 if excel_path: @@ -1110,4 +1236,4 @@ if __name__ == "__main__": # 3. 运行比较 # 如果没有提供excel文件,将自动回退到生成数据模式 - compare_design_methods(args.excel, n_clusters_override=args.clusters) \ No newline at end of file + compare_design_methods(args.excel, n_clusters_override=args.clusters)