优化风电场设计方案对比算法:添加旋转算法和双模式对比
- 新增design_with_rotational_sweep函数:实现旋转优化算法 - 修改compare_design_methods函数: * 将MST结果纳入对比列表 * 每个电缆场景运行Base和Rotational两种算法 * 添加成本和损耗对比显示 * 优化可视化展示和文件输出 - 改进算法选择逻辑:增强簇数计算的智能化 - 更新输出格式:区分不同算法结果并优化显示
This commit is contained in:
256
main.py
256
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 # 计数用
|
||||
|
||||
print(f"DEBUG: 生成了 {cluster_id} 个回路 (簇)")
|
||||
|
||||
# 将 cluster 标记映射回原始 turbines DataFrame (通过 original_id 或 索引匹配)
|
||||
# 这里我们简单地重建 turbines,因为 work_df 包含了所有信息且顺序变了
|
||||
# 为了保持外部一致性,我们把 cluster 列 map 回去
|
||||
cluster_id += 1
|
||||
|
||||
# 建立 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,13 +1068,23 @@ 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} ---")
|
||||
if not current_specs:
|
||||
@@ -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")
|
||||
|
||||
# 输出简报
|
||||
print(f" 总成本: ¥{evaluation['total_cost']:,.2f}")
|
||||
print(f" 总损耗: {evaluation['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
|
||||
)
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user