优化风电场设计方案对比算法:添加旋转算法和双模式对比

- 新增design_with_rotational_sweep函数:实现旋转优化算法
- 修改compare_design_methods函数:
  * 将MST结果纳入对比列表
  * 每个电缆场景运行Base和Rotational两种算法
  * 添加成本和损耗对比显示
  * 优化可视化展示和文件输出
- 改进算法选择逻辑:增强簇数计算的智能化
- 更新输出格式:区分不同算法结果并优化显示
This commit is contained in:
dmy
2026-01-02 00:25:33 +08:00
parent 6cac8806f0
commit b5718a0cc2

258
main.py
View File

@@ -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)
compare_design_methods(args.excel, n_clusters_override=args.clusters)