From 6cac8806f0f76dabf75097b9c98ab74b9db25cfa Mon Sep 17 00:00:00 2001 From: dmy Date: Thu, 1 Jan 2026 23:58:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=A3=8E=E7=94=B5=E5=9C=BA?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=B7=A5=E5=85=B7=EF=BC=9A=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E7=94=B5=E7=BC=86=E8=A7=84=E6=A0=BC=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=A4=9A=E6=96=B9=E6=A1=88=E6=AF=94=E8=BE=83=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新generate_template.py:增加电缆型号至9种,添加Optional字段完善数据结构 - 重构main.py比较流程: * 实现多方案结果存储机制 * 添加交互式DXF导出选择功能(支持单方案/全部导出) * 优化多方案可视化对比展示 * 改进Excel导出功能,整合所有方案数据 * 增强用户交互体验和结果展示 --- generate_template.py | 18 +-- main.py | 338 ++++++++++++++++++++++++++++--------------- 2 files changed, 230 insertions(+), 126 deletions(-) diff --git a/generate_template.py b/generate_template.py index 7875ff8..e95c135 100644 --- a/generate_template.py +++ b/generate_template.py @@ -38,15 +38,15 @@ def create_template(): # Create Cable data cable_data = [ - {'CrossSection': 35, 'Capacity': 150, 'Resistance': 0.524, 'Cost': 80}, - {'CrossSection': 70, 'Capacity': 215, 'Resistance': 0.268, 'Cost': 120}, - {'CrossSection': 95, 'Capacity': 260, 'Resistance': 0.193, 'Cost': 150}, - {'CrossSection': 120, 'Capacity': 295, 'Resistance': 0.153, 'Cost': 180}, - {'CrossSection': 150, 'Capacity': 330, 'Resistance': 0.124, 'Cost': 220}, - {'CrossSection': 185, 'Capacity': 370, 'Resistance': 0.0991, 'Cost': 270}, - {'CrossSection': 240, 'Capacity': 425, 'Resistance': 0.0754, 'Cost': 350}, - {'CrossSection': 300, 'Capacity': 500, 'Resistance': 0.0601, 'Cost': 450}, - {'CrossSection': 400, 'Capacity': 580, 'Resistance': 0.0470, 'Cost': 600} + {'CrossSection': 35, 'Capacity': 150, 'Resistance': 0.524, 'Cost': 80, 'Optional': ''}, + {'CrossSection': 70, 'Capacity': 215, 'Resistance': 0.268, 'Cost': 120, 'Optional': ''}, + {'CrossSection': 95, 'Capacity': 260, 'Resistance': 0.193, 'Cost': 150, 'Optional': ''}, + {'CrossSection': 120, 'Capacity': 295, 'Resistance': 0.153, 'Cost': 180, 'Optional': ''}, + {'CrossSection': 150, 'Capacity': 330, 'Resistance': 0.124, 'Cost': 220, 'Optional': ''}, + {'CrossSection': 185, 'Capacity': 370, 'Resistance': 0.0991, 'Cost': 270, 'Optional': ''}, + {'CrossSection': 240, 'Capacity': 425, 'Resistance': 0.0754, 'Cost': 350, 'Optional': ''}, + {'CrossSection': 300, 'Capacity': 500, 'Resistance': 0.0601, 'Cost': 450, 'Optional': ''}, + {'CrossSection': 400, 'Capacity': 580, 'Resistance': 0.0470, 'Cost': 600, 'Optional': ''} ] df_cables = pd.DataFrame(cable_data) diff --git a/main.py b/main.py index 4a90baf..497ba86 100644 --- a/main.py +++ b/main.py @@ -137,7 +137,7 @@ def load_data_from_excel(file_path): cables_df.columns = [c.replace(' ', '').capitalize() for c in cables_df.columns] # Handle 'Cross Section' vs 'CrossSection' # 尝试匹配列 - # 目标格式: (截面mm², 载流量A, 电阻Ω/km, 基准价格元/m) + # 目标格式: (截面mm², 载流量A, 电阻Ω/km, 基准价格元/m, 是否可选) specs = [] for _, row in cables_df.iterrows(): # 容错处理列名 @@ -145,9 +145,11 @@ def load_data_from_excel(file_path): capacity = row.get('Capacity', row.get('Current', 0)) resistance = row.get('Resistance', 0) cost = row.get('Cost', row.get('Price', 0)) + optional_val = str(row.get('Optional', '')).strip().upper() + is_optional = (optional_val == 'Y') if section > 0 and capacity > 0: - specs.append((section, capacity, resistance, cost)) + specs.append((section, capacity, resistance, cost, is_optional)) if specs: specs.sort(key=lambda x: x[1]) # 按载流量排序 @@ -655,7 +657,6 @@ def export_to_dxf(turbines, substation, connections_details, filename): source, target = conn['source'], conn['target'] section = conn['cable']['cross_section'] - # 获取坐标 if source == 'substation': p1 = (substation[0, 0], substation[0, 1]) else: @@ -726,6 +727,64 @@ def export_to_excel(connections_details, filename): except Exception as e: print(f"导出Excel失败: {e}") +# 6.6 导出多方案对比Excel报表 +def export_all_scenarios_to_excel(results, filename): + """ + 导出所有方案的对比结果到 Excel + :param results: 包含各方案评估结果的列表 + :param filename: 输出文件路径 + """ + try: + with pd.ExcelWriter(filename) as writer: + # 1. 总览 Sheet + summary_data = [] + for res in results: + # 获取回路数 + n_circuits = 0 + if 'turbines' in res and 'cluster' in res['turbines'].columns: + n_circuits = res['turbines']['cluster'].nunique() + + summary_data.append({ + 'Scenario': res['name'], + 'Total Cost (¥)': res['cost'], + 'Total Loss (kW)': res['loss'], + 'Num Circuits': n_circuits, + # 计算电缆统计 + 'Total Cable Length (m)': sum(d['length'] for d in res['eval']['details']) + }) + + pd.DataFrame(summary_data).to_excel(writer, sheet_name='Comparison Summary', index=False) + + # 2. 每个方案的详细 Sheet + for res in results: + # 清理 Sheet 名称 + safe_name = res['name'].replace(':', '').replace('/', '-').replace('\\', '-') + # 截断过长的名称 (Excel限制31字符) + if len(safe_name) > 25: + safe_name = safe_name[:25] + + details = res['eval']['details'] + data = [] + for conn in details: + data.append({ + 'Source': conn['source'], + 'Target': conn['target'], + 'Horizontal Length (m)': conn['horizontal_length'], + 'Vertical Length (m)': conn['vertical_length'], + 'Effective Length (m)': conn['length'], + 'Cable Type (mm²)': conn['cable']['cross_section'], + 'Current (A)': conn['cable']['current'], + 'Power (MW)': conn['power'], + 'Resistance (Ω)': conn['cable']['resistance'], + 'Cost (¥)': conn['cable']['cost'] + }) + df = pd.DataFrame(data) + df.to_excel(writer, sheet_name=safe_name, index=False) + + print(f"成功导出包含所有方案的Excel文件: {filename}") + except Exception as e: + print(f"导出Excel失败: {e}") + # 6. 可视化函数 def visualize_design(turbines, substation, connections, title, ax=None, show_costs=True): """可视化集电线路设计方案""" @@ -840,8 +899,8 @@ def visualize_design(turbines, substation, connections, title, ax=None, show_cos # 7. 主函数:比较两种设计方法 def compare_design_methods(excel_path=None, n_clusters_override=None): """ - 比较MST和K-means两种设计方法 (海上风电场场景) - :param excel_path: Excel文件路径,如果提供则从文件读取数据 + 比较MST和三种电缆方案下的K-means设计方法 + :param excel_path: Excel文件路径 :param n_clusters_override: 可选,手动指定簇的数量 """ cable_specs = None @@ -855,146 +914,191 @@ def compare_design_methods(excel_path=None, n_clusters_override=None): return compare_design_methods(excel_path=None, n_clusters_override=n_clusters_override) else: print("正在生成海上风电场数据 (规则阵列布局)...") - # 使用规则布局,间距800m turbines, substation = generate_wind_farm_data(n_turbines=30, layout='grid', spacing=800) scenario_title = "Offshore Wind Farm (Grid Layout)" is_offshore = True - # 方法1:最小生成树 - # 注意:MST方法在不考虑容量约束时,可能会导致根部线路严重过载 + # 准备三种电缆方案 + # 原始 specs 是 5 元素元组: (section, capacity, resistance, cost, is_optional) + # 下游函数期望 4 元素元组: (section, capacity, resistance, cost) + if cable_specs: + # 方案 1: 不含 Optional='Y' (Standard) + specs_1 = [s[:4] for s in cable_specs if not s[4]] + + # 方案 2: 含 Optional='Y' (All) + specs_2 = [s[:4] for s in cable_specs] + + # 方案 3: 基于方案 1,删掉截面最大的一种 + # cable_specs 已按 capacity 排序,假设 capacity 与 section 正相关 + specs_3 = specs_1[:-1] if len(specs_1) > 1 else list(specs_1) + else: + # 默认电缆库 + default_specs = [ + (35, 150, 0.524, 80), (70, 215, 0.268, 120), (95, 260, 0.193, 150), + (120, 295, 0.153, 180), (150, 330, 0.124, 220), (185, 370, 0.0991, 270), + (240, 425, 0.0754, 350), (300, 500, 0.0601, 450), (400, 580, 0.0470, 600) + ] + specs_1 = default_specs + specs_2 = default_specs + specs_3 = default_specs[:-1] + + scenarios = [ + ("Scenario 1 (Standard)", specs_1), + ("Scenario 2 (With Optional)", specs_2), + ("Scenario 3 (No Max)", specs_3) + ] + + # 1. MST 方法作为基准 (使用 Scenario 1) mst_connections = design_with_mst(turbines, substation) - mst_evaluation = evaluate_design(turbines, mst_connections, substation, cable_specs=cable_specs, is_offshore=is_offshore, method_name="MST Method") + mst_evaluation = evaluate_design(turbines, mst_connections, substation, cable_specs=specs_1, is_offshore=is_offshore, method_name="MST Method") - # 方法2:K-means聚类 (容量受限聚类) - # 计算总功率和所需的最小回路数 - total_power = turbines['power'].sum() - max_cable_mw = get_max_cable_capacity_mw(cable_specs=cable_specs) + # 准备画布 2x2 + fig, axes = plt.subplots(2, 2, figsize=(20, 18)) + axes = axes.flatten() - # 获取电缆型号数量 - if cable_specs is not None: - n_cable_types = len(cable_specs) - else: - # 对应 evaluate_design 中的默认电缆规格库数量 - n_cable_types = 9 - - # 允许指定簇的数量,如果设置为 None 则自动计算 - if n_clusters_override is not None: - n_clusters = n_clusters_override - min_clusters_needed = int(np.ceil(total_power / max_cable_mw)) - print(f"使用手动指定的回路数(簇数): {n_clusters} (理论最小需求 {min_clusters_needed})") - else: - # 新逻辑:风机数量 / 电缆型号数量,向上取整 - n_clusters = int(np.ceil(len(turbines) / n_cable_types)) - min_clusters_needed = int(np.ceil(total_power / max_cable_mw)) - # 确保回路数不低于容量需求的理论最小值 - if n_clusters < min_clusters_needed: - print(f"WARNING: 根据风机/型号计算的簇数({n_clusters})低于理论最小需求({min_clusters_needed}),已调整为理论最小值。") - n_clusters = min_clusters_needed - - if len(turbines) < n_clusters: # 避免簇数多于风机数 - n_clusters = len(turbines) - - print(f"系统设计参数: 总功率 {total_power:.1f} MW, 单回路最大容量 {max_cable_mw:.1f} MW, 型号数量 {n_cable_types}") - print(f"K-means 簇数设定为: {n_clusters}") - - # 替换为带容量约束的扫描算法 - kmeans_connections, clustered_turbines = design_with_capacitated_sweep(turbines.copy(), substation, cable_specs=cable_specs) - kmeans_evaluation = evaluate_design(turbines, kmeans_connections, substation, cable_specs=cable_specs, is_offshore=is_offshore, method_name="Capacitated Sweep") - - # 创建结果比较 - results = { - 'MST Method': mst_evaluation, - 'Capacitated Sweep': kmeans_evaluation - } - - # 可视化 - fig, axes = plt.subplots(1, 2, figsize=(20, 10)) - - # 可视化MST方法 + # 绘制 MST visualize_design(turbines, substation, mst_evaluation['details'], - f"MST Design - {scenario_title}\nTotal Cost: ¥{mst_evaluation['total_cost']/10000:.2f}万\nTotal Loss: {mst_evaluation['total_loss']:.2f} kW", + f"MST Method (Standard Cables)\nTotal Cost: ¥{mst_evaluation['total_cost']/10000:.2f}万", ax=axes[0]) + + print(f"\n===== 开始比较电缆方案 (基于 Capacitated Sweep) =====") - # 可视化K-means方法 (现在是 Capacitated Sweep) - # 获取实际生成的簇数 - n_actual_clusters = clustered_turbines['cluster'].nunique() - visualize_design(clustered_turbines, substation, kmeans_evaluation['details'], - f"Capacitated Sweep ({n_actual_clusters} circuits) - {scenario_title}\nTotal Cost: ¥{kmeans_evaluation['total_cost']/10000:.2f}万\nTotal Loss: {kmeans_evaluation['total_loss']:.2f} kW", - ax=axes[1]) + best_cost = float('inf') + best_result = None + comparison_results = [] + + for i, (name, current_specs) in enumerate(scenarios): + print(f"\n--- {name} ---") + if not current_specs: + print(" 无可用电缆,跳过。") + continue + + # 计算参数 + total_power = turbines['power'].sum() + max_cable_mw = get_max_cable_capacity_mw(cable_specs=current_specs) + n_cable_types = len(current_specs) + + # 确定簇数 + 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}。设计可能会失败或严重过载。") + else: + # 自动计算:取 (理论最小需求) 和 (基于电缆型号分级估算) 的较大值 + min_needed = int(np.ceil(total_power / max_cable_mw)) + # 这里的逻辑是:如果电缆级差很密,可能不需要那么多回路;如果电缆很大,回路可以少。 + # 原有的逻辑是 len(turbines)/n_cable_types,这只是一个经验值。 + # 我们主要保证满足容量: + n_clusters = min_needed + # 稍微增加一点裕度,避免因为离散分布导致最后一点功率塞不进 + # 或者保持原有的启发式逻辑,取最大值 + 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( + turbines.copy(), substation, cable_specs=current_specs + ) + + # 评估 + evaluation = evaluate_design( + turbines, conns, substation, cable_specs=current_specs, + is_offshore=is_offshore, method_name=name + ) + + # 记录结果 + comparison_results.append({ + 'name': name, + 'cost': evaluation['total_cost'], + 'loss': evaluation['total_loss'], + 'eval': evaluation, + 'turbines': clustered_turbines, + 'specs': current_specs + }) + + # 输出简报 + print(f" 总成本: ¥{evaluation['total_cost']:,.2f}") + print(f" 总损耗: {evaluation['total_loss']:.2f} kW") + + # 记录最佳 + if evaluation['total_cost'] < best_cost: + best_cost = evaluation['total_cost'] + best_result = comparison_results[-1] + + # 可视化 (axes 1, 2, 3) + 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]) + plt.tight_layout() output_filename = 'wind_farm_design_comparison.png' plt.savefig(output_filename, dpi=300) + print(f"\n比较图已保存至: {output_filename}") - # 导出最佳方案 DXF 和 Excel + # 准备文件路径 if excel_path: base_name = os.path.splitext(os.path.basename(excel_path))[0] dir_name = os.path.dirname(excel_path) - # 如果 dir_name 为空(即当前目录),则直接使用文件名 dxf_filename = os.path.join(dir_name, f"{base_name}_design.dxf") excel_out_filename = os.path.join(dir_name, f"{base_name}_design.xlsx") else: dxf_filename = 'wind_farm_design.dxf' excel_out_filename = 'wind_farm_design.xlsx' - - # Use the connection details from the best evaluation - # Note: We need the turbine DataFrame corresponding to the best method to get cluster IDs if possible, - # but export_to_dxf mainly needs coordinates which are constant. - export_to_dxf(clustered_turbines, substation, kmeans_evaluation['details'], dxf_filename) - export_to_excel(kmeans_evaluation['details'], excel_out_filename) - - # plt.show() - - # 打印详细结果 - print(f"\n===== 海上风电场设计方案比较 ({'导入数据' if excel_path else '自动生成'}) =====") - for method, eval_data in results.items(): - print(f"\n{method}:") - print(f" 总成本: ¥{eval_data['total_cost']:,.2f} ({eval_data['total_cost']/10000:.2f}万元)") - print(f" 预估总损耗: {eval_data['total_loss']:.2f} kW") - print(f" 连接数量: {eval_data['num_connections']}") - # Calculate additional metrics for K-means (Capacitated Sweep) - print("\n===== K-Means (Capacitated Sweep) 详细统计 =====") - - # 1. 总回路数 - # The 'cluster' column in clustered_turbines holds the circuit ID. - num_circuits = clustered_turbines['cluster'].nunique() - print(f"总回路数: {num_circuits}") - - # 2. 每种型号电缆长度 - raw_horizontal_lengths = defaultdict(float) # 仅风机间 - effective_lengths = defaultdict(float) # 机箱变之间 (含垂直+裕度) - max_current = 0.0 - total_vertical_length = 0.0 - - for conn in kmeans_evaluation['details']: - section = conn['cable']['cross_section'] + # 导出所有方案到同一个 Excel + if comparison_results: + export_all_scenarios_to_excel(comparison_results, excel_out_filename) - # Accumulate - raw_horizontal_lengths[section] += conn['horizontal_length'] - effective_lengths[section] += conn['length'] - total_vertical_length += conn['vertical_length'] - - # Max Current - current = conn['cable']['current'] - if current > max_current: - max_current = current + # 交互式选择导出 DXF + print("\n===== 方案选择 =====") + best_idx = 0 + for i, res in enumerate(comparison_results): + if res['cost'] < comparison_results[best_idx]['cost']: + best_idx = i + print(f" {i+1}. {res['name']} - Cost: ¥{res['cost']:,.2f}") - print("每种型号电缆长度 (仅风机间水平距离):") - for section in sorted(raw_horizontal_lengths.keys()): - print(f" {section}mm²: {raw_horizontal_lengths[section]:.2f} m") - - print("机箱变之间电缆长度 (含垂直高度及1.03裕度):") - for section in sorted(effective_lengths.keys()): - print(f" {section}mm²: {effective_lengths[section]:.2f} m") - - print(f"其中电缆上下风机总长度: {total_vertical_length:.2f} m") + print(f"推荐方案: {comparison_results[best_idx]['name']} (默认)") - # 3. 最大支路电流 - print(f"最大支路电流: {max_current:.2f} A") - - return results + try: + choice_str = input(f"请输入要导出DXF的方案编号 (1-{len(comparison_results)}),或输入 'A' 导出全部: ").strip() + + if choice_str.upper() == 'A': + print("正在导出所有方案...") + base_dxf_name, ext = os.path.splitext(dxf_filename) + for res in comparison_results: + # 生成文件名安全后缀 + safe_suffix = res['name'].replace(' ', '_').replace(':', '').replace('(', '').replace(')', '').replace('/', '-') + current_filename = f"{base_dxf_name}_{safe_suffix}{ext}" + print(f" 导出 '{res['name']}' -> {current_filename}") + export_to_dxf(res['turbines'], substation, res['eval']['details'], current_filename) + else: + if not choice_str: + choice = best_idx + else: + choice = int(choice_str) - 1 + if choice < 0 or choice >= len(comparison_results): + print("输入编号无效,将使用默认推荐方案。") + choice = best_idx + + selected_res = comparison_results[choice] + print(f"正在导出 '{selected_res['name']}' 到 DXF: {dxf_filename} ...") + export_to_dxf(selected_res['turbines'], substation, selected_res['eval']['details'], dxf_filename) + except Exception as e: + print(f"输入处理出错: {e},将使用默认推荐方案。") + selected_res = comparison_results[best_idx] + print(f"正在导出 '{selected_res['name']}' 到 DXF: {dxf_filename} ...") + export_to_dxf(selected_res['turbines'], substation, selected_res['eval']['details'], dxf_filename) + + return comparison_results # 8. 执行比较 if __name__ == "__main__":