更新风电场设计工具:扩展电缆规格并优化多方案比较功能

- 更新generate_template.py:增加电缆型号至9种,添加Optional字段完善数据结构
- 重构main.py比较流程:
  * 实现多方案结果存储机制
  * 添加交互式DXF导出选择功能(支持单方案/全部导出)
  * 优化多方案可视化对比展示
  * 改进Excel导出功能,整合所有方案数据
  * 增强用户交互体验和结果展示
This commit is contained in:
dmy
2026-01-01 23:58:03 +08:00
parent 34b0d70309
commit 6cac8806f0
2 changed files with 230 additions and 126 deletions

View File

@@ -38,15 +38,15 @@ def create_template():
# Create Cable data # Create Cable data
cable_data = [ cable_data = [
{'CrossSection': 35, 'Capacity': 150, 'Resistance': 0.524, 'Cost': 80}, {'CrossSection': 35, 'Capacity': 150, 'Resistance': 0.524, 'Cost': 80, 'Optional': ''},
{'CrossSection': 70, 'Capacity': 215, 'Resistance': 0.268, 'Cost': 120}, {'CrossSection': 70, 'Capacity': 215, 'Resistance': 0.268, 'Cost': 120, 'Optional': ''},
{'CrossSection': 95, 'Capacity': 260, 'Resistance': 0.193, 'Cost': 150}, {'CrossSection': 95, 'Capacity': 260, 'Resistance': 0.193, 'Cost': 150, 'Optional': ''},
{'CrossSection': 120, 'Capacity': 295, 'Resistance': 0.153, 'Cost': 180}, {'CrossSection': 120, 'Capacity': 295, 'Resistance': 0.153, 'Cost': 180, 'Optional': ''},
{'CrossSection': 150, 'Capacity': 330, 'Resistance': 0.124, 'Cost': 220}, {'CrossSection': 150, 'Capacity': 330, 'Resistance': 0.124, 'Cost': 220, 'Optional': ''},
{'CrossSection': 185, 'Capacity': 370, 'Resistance': 0.0991, 'Cost': 270}, {'CrossSection': 185, 'Capacity': 370, 'Resistance': 0.0991, 'Cost': 270, 'Optional': ''},
{'CrossSection': 240, 'Capacity': 425, 'Resistance': 0.0754, 'Cost': 350}, {'CrossSection': 240, 'Capacity': 425, 'Resistance': 0.0754, 'Cost': 350, 'Optional': ''},
{'CrossSection': 300, 'Capacity': 500, 'Resistance': 0.0601, 'Cost': 450}, {'CrossSection': 300, 'Capacity': 500, 'Resistance': 0.0601, 'Cost': 450, 'Optional': ''},
{'CrossSection': 400, 'Capacity': 580, 'Resistance': 0.0470, 'Cost': 600} {'CrossSection': 400, 'Capacity': 580, 'Resistance': 0.0470, 'Cost': 600, 'Optional': ''}
] ]
df_cables = pd.DataFrame(cable_data) df_cables = pd.DataFrame(cable_data)

330
main.py
View File

@@ -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' 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 = [] specs = []
for _, row in cables_df.iterrows(): 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)) capacity = row.get('Capacity', row.get('Current', 0))
resistance = row.get('Resistance', 0) resistance = row.get('Resistance', 0)
cost = row.get('Cost', row.get('Price', 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: if section > 0 and capacity > 0:
specs.append((section, capacity, resistance, cost)) specs.append((section, capacity, resistance, cost, is_optional))
if specs: if specs:
specs.sort(key=lambda x: x[1]) # 按载流量排序 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'] source, target = conn['source'], conn['target']
section = conn['cable']['cross_section'] section = conn['cable']['cross_section']
# 获取坐标
if source == 'substation': if source == 'substation':
p1 = (substation[0, 0], substation[0, 1]) p1 = (substation[0, 0], substation[0, 1])
else: else:
@@ -726,6 +727,64 @@ def export_to_excel(connections_details, filename):
except Exception as e: except Exception as e:
print(f"导出Excel失败: {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. 可视化函数 # 6. 可视化函数
def visualize_design(turbines, substation, connections, title, ax=None, show_costs=True): 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. 主函数:比较两种设计方法 # 7. 主函数:比较两种设计方法
def compare_design_methods(excel_path=None, n_clusters_override=None): def compare_design_methods(excel_path=None, n_clusters_override=None):
""" """
比较MST和K-means两种设计方法 (海上风电场场景) 比较MST和三种电缆方案下的K-means设计方法
:param excel_path: Excel文件路径,如果提供则从文件读取数据 :param excel_path: Excel文件路径
:param n_clusters_override: 可选,手动指定簇的数量 :param n_clusters_override: 可选,手动指定簇的数量
""" """
cable_specs = None 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) return compare_design_methods(excel_path=None, n_clusters_override=n_clusters_override)
else: else:
print("正在生成海上风电场数据 (规则阵列布局)...") print("正在生成海上风电场数据 (规则阵列布局)...")
# 使用规则布局间距800m
turbines, substation = generate_wind_farm_data(n_turbines=30, layout='grid', spacing=800) turbines, substation = generate_wind_farm_data(n_turbines=30, layout='grid', spacing=800)
scenario_title = "Offshore Wind Farm (Grid Layout)" scenario_title = "Offshore Wind Farm (Grid Layout)"
is_offshore = True 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_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")
# 方法2K-means聚类 (容量受限聚类) # 准备画布 2x2
# 计算总功率和所需的最小回路数 fig, axes = plt.subplots(2, 2, figsize=(20, 18))
total_power = turbines['power'].sum() axes = axes.flatten()
max_cable_mw = get_max_cable_capacity_mw(cable_specs=cable_specs)
# 获取电缆型号数量 # 绘制 MST
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方法
visualize_design(turbines, substation, mst_evaluation['details'], 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]) ax=axes[0])
# 可视化K-means方法 (现在是 Capacitated Sweep) print(f"\n===== 开始比较电缆方案 (基于 Capacitated Sweep) =====")
# 获取实际生成的簇数
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() n_actual_clusters = clustered_turbines['cluster'].nunique()
visualize_design(clustered_turbines, substation, kmeans_evaluation['details'], title = f"{name} ({n_actual_clusters} circuits)\nCost: ¥{evaluation['total_cost']/10000:.2f}万 | Loss: {evaluation['total_loss']:.2f} kW"
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", visualize_design(clustered_turbines, substation, evaluation['details'], title, ax=axes[ax_idx])
ax=axes[1])
plt.tight_layout() plt.tight_layout()
output_filename = 'wind_farm_design_comparison.png' output_filename = 'wind_farm_design_comparison.png'
plt.savefig(output_filename, dpi=300) plt.savefig(output_filename, dpi=300)
print(f"\n比较图已保存至: {output_filename}")
# 导出最佳方案 DXF 和 Excel # 准备文件路径
if excel_path: if excel_path:
base_name = os.path.splitext(os.path.basename(excel_path))[0] base_name = os.path.splitext(os.path.basename(excel_path))[0]
dir_name = os.path.dirname(excel_path) dir_name = os.path.dirname(excel_path)
# 如果 dir_name 为空(即当前目录),则直接使用文件名
dxf_filename = os.path.join(dir_name, f"{base_name}_design.dxf") 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") excel_out_filename = os.path.join(dir_name, f"{base_name}_design.xlsx")
else: else:
dxf_filename = 'wind_farm_design.dxf' dxf_filename = 'wind_farm_design.dxf'
excel_out_filename = 'wind_farm_design.xlsx' excel_out_filename = 'wind_farm_design.xlsx'
# Use the connection details from the best evaluation # 导出所有方案到同一个 Excel
# Note: We need the turbine DataFrame corresponding to the best method to get cluster IDs if possible, if comparison_results:
# but export_to_dxf mainly needs coordinates which are constant. export_all_scenarios_to_excel(comparison_results, excel_out_filename)
export_to_dxf(clustered_turbines, substation, kmeans_evaluation['details'], dxf_filename)
export_to_excel(kmeans_evaluation['details'], excel_out_filename)
# plt.show() # 交互式选择导出 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(f"推荐方案: {comparison_results[best_idx]['name']} (默认)")
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) try:
print("\n===== K-Means (Capacitated Sweep) 详细统计 =====") choice_str = input(f"请输入要导出DXF的方案编号 (1-{len(comparison_results)}),或输入 'A' 导出全部: ").strip()
# 1. 总回路数 if choice_str.upper() == 'A':
# The 'cluster' column in clustered_turbines holds the circuit ID. print("正在导出所有方案...")
num_circuits = clustered_turbines['cluster'].nunique() base_dxf_name, ext = os.path.splitext(dxf_filename)
print(f"总回路数: {num_circuits}") 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
# 2. 每种型号电缆长度 selected_res = comparison_results[choice]
raw_horizontal_lengths = defaultdict(float) # 仅风机间 print(f"正在导出 '{selected_res['name']}' 到 DXF: {dxf_filename} ...")
effective_lengths = defaultdict(float) # 机箱变之间 (含垂直+裕度) export_to_dxf(selected_res['turbines'], substation, selected_res['eval']['details'], dxf_filename)
max_current = 0.0 except Exception as e:
total_vertical_length = 0.0 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)
for conn in kmeans_evaluation['details']: return comparison_results
section = conn['cable']['cross_section']
# 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
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")
# 3. 最大支路电流
print(f"最大支路电流: {max_current:.2f} A")
return results
# 8. 执行比较 # 8. 执行比较
if __name__ == "__main__": if __name__ == "__main__":