Compare commits
10 Commits
2f70b2fc72
...
d563905f28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d563905f28 | ||
|
|
b5718a0cc2 | ||
|
|
6cac8806f0 | ||
|
|
34b0d70309 | ||
|
|
6454a2c01e | ||
|
|
2d50ab0df0 | ||
|
|
41e3cf355c | ||
|
|
e6d98297b1 | ||
|
|
e7e12745d1 | ||
|
|
4db9d138b8 |
9
GEMINI.md
Normal file
9
GEMINI.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
运行shell时使用powershell模式。
|
||||||
|
运行python代码前加载uv环境。
|
||||||
|
编写代码时,尽可能多加注释。
|
||||||
|
|
||||||
|
修改工程下的任何代码不需要询问我的同意。
|
||||||
|
在工程下执行shell,不需要我的同意。
|
||||||
|
在工程下执行任何命令,不需要我的同意。
|
||||||
|
|
||||||
|
Please talk to me in Chinese.
|
||||||
146
README.md
146
README.md
@@ -1,85 +1,73 @@
|
|||||||
# 海上风电场集电线路设计优化工具
|
# 海上风电场集电系统设计工具
|
||||||
|
|
||||||
## 项目简介
|
一个用于设计和优化海上风电场集电系统的Python工具,支持多种布局算法和电缆优化方案。
|
||||||
|
|
||||||
这是一个用于海上风电场集电线路拓扑设计和优化的Python工具。它专注于解决大规模海上风电场的集电系统规划问题,通过算法比较不同设计方案的经济性和技术指标。
|
## 功能特性
|
||||||
|
|
||||||
本项目特别针对**海上风电**场景进行了优化,考虑了海缆的高昂成本、大功率风机(6-10MW)以及严格的电缆载流量约束。
|
- 🌊 多种风机布局生成(随机分布、规则网格)
|
||||||
|
- 🔌 多种集电系统设计算法:
|
||||||
|
- 最小生成树(MST)算法
|
||||||
|
- K-means聚类算法
|
||||||
|
- 容量扫描算法(Capacitated Sweep)
|
||||||
|
- 旋转优化算法(Rotational Sweep)
|
||||||
|
- 📊 多方案对比分析和可视化
|
||||||
|
- 📋 自动导出DXF图纸和Excel报告
|
||||||
|
- 🔧 智能电缆规格选择和成本优化
|
||||||
|
|
||||||
## 核心功能
|
## 安装依赖
|
||||||
|
|
||||||
### 1. 多种布局生成与导入
|
|
||||||
- **自动生成**:支持生成规则的矩阵式(Grid)风机布局,模拟海上风电场常见排布。
|
|
||||||
- **Excel导入**:支持从 `coordinates.xlsx` 导入自定义的风机和升压站坐标。
|
|
||||||
- 格式要求:包含 `Type` (Turbine/Substation), `ID`, `X`, `Y`, `Power` 列。
|
|
||||||
|
|
||||||
### 2. 智能拓扑优化算法
|
|
||||||
- **最小生成树 (MST)**:
|
|
||||||
- 计算全局最短路径长度。
|
|
||||||
- *注意*:在大规模风电场中,纯MST往往会导致根部电缆严重过载,仅作为理论最短路径参考。
|
|
||||||
- **扇区聚类 (Angular K-means)**:
|
|
||||||
- **无交叉设计**:基于角度(扇区)进行聚类,从几何上杜绝不同回路间的电缆交叉。
|
|
||||||
- **容量约束**:自动计算所需的最小回路数(Clusters),确保每条集电线路的总功率不超过海缆极限。
|
|
||||||
|
|
||||||
### 3. 精细化电气计算与选型
|
|
||||||
- **动态电缆选型**:
|
|
||||||
- 基于实际潮流计算(Power Flow),为每一段线路选择最经济且满足载流量的电缆。
|
|
||||||
- 规格库:覆盖 35mm² 至 400mm² 海缆。
|
|
||||||
- 参数:电压等级 **66kV**,功率因数 0.95。
|
|
||||||
- **成本与损耗评估**:
|
|
||||||
- 考虑海缆材料及敷设成本(约为陆缆的5倍)。
|
|
||||||
- 计算全场集电线路的 $I^2R$ 损耗。
|
|
||||||
|
|
||||||
### 4. 工程级可视化与输出
|
|
||||||
- **可视化图表**:
|
|
||||||
- 生成直观的拓扑连接图。
|
|
||||||
- **颜色编码**:使用不同颜色和粗细区分不同截面的电缆(如绿色细线为35mm²,红色粗线为400mm²)。
|
|
||||||
- 自动保存为高清 PNG 图片。
|
|
||||||
- **CAD (DXF) 导出**:
|
|
||||||
- 使用 `ezdxf` 生成 `.dxf` 文件。
|
|
||||||
- 分层管理:风机、升压站、各规格电缆分层显示,可直接导入 AutoCAD 进行后续工程设计。
|
|
||||||
|
|
||||||
## 安装说明
|
|
||||||
|
|
||||||
### 环境要求
|
|
||||||
- Python >= 3.10
|
|
||||||
- 推荐使用 [uv](https://github.com/astral-sh/uv) 进行依赖管理。
|
|
||||||
|
|
||||||
### 安装依赖
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 使用 uv (推荐)
|
pip install numpy pandas matplotlib scikit-learn scipy networkx
|
||||||
uv add numpy pandas matplotlib scipy scikit-learn networkx ezdxf openpyxl
|
|
||||||
|
|
||||||
# 或使用 pip
|
|
||||||
pip install numpy pandas matplotlib scipy scikit-learn networkx ezdxf openpyxl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
### 1. 运行主程序
|
### 基本用法
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 使用 uv
|
|
||||||
uv run main.py
|
|
||||||
|
|
||||||
# 或直接运行
|
|
||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 数据输入模式
|
### 指定数据文件
|
||||||
|
|
||||||
程序会自动检测当前目录下是否存在 `coordinates.xlsx`:
|
```bash
|
||||||
|
python main.py --excel wind_farm_coordinates.xlsx
|
||||||
|
```
|
||||||
|
|
||||||
- **存在**:优先读取 Excel 文件中的坐标数据进行计算。
|
### 覆盖默认簇数
|
||||||
- **不存在**:自动生成 30 台风机的规则布局(Grid Layout)进行演示。
|
|
||||||
|
|
||||||
### 3. 结果输出
|
```bash
|
||||||
|
python main.py --clusters 20
|
||||||
|
```
|
||||||
|
|
||||||
程序运行结束后会:
|
## 算法说明
|
||||||
1. 在终端打印详细的成本、损耗及电缆统计数据。
|
|
||||||
2. 弹窗显示拓扑对比图,并保存为 `wind_farm_design_imported.png` (或 `offshore_...png`)。
|
### 1. MST Method(最小生成树)
|
||||||
3. 生成 CAD 图纸文件 `wind_farm_design.dxf`。
|
- 使用最小生成树连接所有风机到海上变电站
|
||||||
|
- 简单高效,适合初步设计
|
||||||
|
|
||||||
|
### 2. K-means Clustering
|
||||||
|
- 将风机分组到多个回路中
|
||||||
|
- 平衡每回路的功率分配
|
||||||
|
|
||||||
|
### 3. Capacitated Sweep(容量扫描)
|
||||||
|
- 考虑电缆容量约束的智能分组
|
||||||
|
- 支持多种电缆规格自动选择
|
||||||
|
|
||||||
|
### 4. Rotational Sweep(旋转优化)
|
||||||
|
- 在容量扫描基础上进行旋转优化
|
||||||
|
- 进一步降低总成本和损耗
|
||||||
|
|
||||||
|
## 输出文件
|
||||||
|
|
||||||
|
1. **可视化图片**:`wind_farm_design_comparison.png`
|
||||||
|
- 不同算法的设计方案对比图
|
||||||
|
|
||||||
|
2. **CAD图纸**:`wind_farm_design.dxf`
|
||||||
|
- 可导入CAD软件的详细设计图纸
|
||||||
|
|
||||||
|
3. **数据报告**:`wind_farm_design.xlsx`
|
||||||
|
- 包含所有方案的详细技术参数和成本分析
|
||||||
|
|
||||||
## 关键参数说明
|
## 关键参数说明
|
||||||
|
|
||||||
@@ -91,18 +79,36 @@ POWER_FACTOR = 0.95 # 功率因数
|
|||||||
cost_multiplier = 5.0 # 海缆相对于陆缆的成本倍数
|
cost_multiplier = 5.0 # 海缆相对于陆缆的成本倍数
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 电缆规格配置
|
||||||
|
|
||||||
|
项目支持多种电缆规格,可在 `generate_template.py` 中配置:
|
||||||
|
|
||||||
|
| 截面积(mm²) | 容量(MW) | 电阻(Ω/km) | 成本(元/m) |
|
||||||
|
|-------------|----------|------------|------------|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
## 输出示例
|
## 输出示例
|
||||||
|
|
||||||
```text
|
```text
|
||||||
系统设计参数: 总功率 2000.0 MW, 单回路最大容量 50.4 MW
|
===== 开始比较电缆方案 =====
|
||||||
计算建议回路数(簇数): 48 (最小需求 40)
|
|
||||||
|
|
||||||
[Sector Clustering] 电缆统计:
|
--- All Cables (Base) ---
|
||||||
70mm²: 48 条
|
[Base] Cost: ¥12,456,789.12 | Loss: 234.56 kW
|
||||||
185mm²: 37 条
|
[Rotational] Cost: ¥12,234,567.89 | Loss: 223.45 kW
|
||||||
400mm²: 40 条
|
|
||||||
|
|
||||||
成功导出DXF文件: wind_farm_design.dxf
|
--- High Current (Base) ---
|
||||||
|
[Base] Cost: ¥11,987,654.32 | Loss: 245.67 kW
|
||||||
|
[Rotational] Cost: ¥11,876,543.21 | Loss: 234.56 kW
|
||||||
|
|
||||||
|
推荐方案: High Current (Rotational) (默认)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|||||||
241
esau_williams.py
Normal file
241
esau_williams.py
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from scipy.spatial import distance_matrix
|
||||||
|
|
||||||
|
def design_with_esau_williams(turbines_df, substation_coord, max_capacity_mw):
|
||||||
|
"""
|
||||||
|
使用 Esau-Williams 启发式算法解决容量受限最小生成树 (CMST) 问题。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
turbines_df: 包含风机信息的 DataFrame (必须包含 'x', 'y', 'power', 'id')
|
||||||
|
substation_coord: 升压站坐标 (x, y)
|
||||||
|
max_capacity_mw: 单根电缆最大允许功率 (MW)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
connections: 连接列表 [(source, target, length), ...]
|
||||||
|
turbines_with_cluster: 带有 'cluster' 列的 turbines DataFrame (用于兼容性)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 数据准备
|
||||||
|
n_turbines = len(turbines_df)
|
||||||
|
coords = turbines_df[['x', 'y']].values
|
||||||
|
powers = turbines_df['power'].values
|
||||||
|
ids = turbines_df['id'].values
|
||||||
|
|
||||||
|
# 升压站坐标
|
||||||
|
if substation_coord.ndim > 1:
|
||||||
|
sx, sy = substation_coord[0][0], substation_coord[0][1]
|
||||||
|
else:
|
||||||
|
sx, sy = substation_coord[0], substation_coord[1]
|
||||||
|
|
||||||
|
# 1. 计算距离矩阵
|
||||||
|
# 风机到风机
|
||||||
|
dist_matrix = distance_matrix(coords, coords)
|
||||||
|
# 风机到升压站
|
||||||
|
dists_to_sub = np.sqrt((coords[:, 0] - sx)**2 + (coords[:, 1] - sy)**2)
|
||||||
|
|
||||||
|
# 2. 初始化组件 (Components)
|
||||||
|
# 初始状态下,每个风机是一个独立的组件,直接连接到升压站
|
||||||
|
# 为了方便查找,我们维护一个 components 字典
|
||||||
|
# key: component_root_id (代表该组件的唯一标识)
|
||||||
|
# value: {
|
||||||
|
# 'members': {node_idx, ...},
|
||||||
|
# 'total_power': float,
|
||||||
|
# 'gate_node': int (连接到升压站的节点索引),
|
||||||
|
# 'gate_cost': float (gate_node 到升压站的距离)
|
||||||
|
# }
|
||||||
|
|
||||||
|
components = {}
|
||||||
|
for i in range(n_turbines):
|
||||||
|
components[i] = {
|
||||||
|
'members': {i},
|
||||||
|
'total_power': powers[i],
|
||||||
|
'gate_node': i,
|
||||||
|
'gate_cost': dists_to_sub[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 记录已经建立的连接 (不包括通往升压站的默认连接)
|
||||||
|
# 格式: (u, v, length)
|
||||||
|
established_edges = []
|
||||||
|
# 记录已建立连接的坐标,用于交叉检查: [((x1, y1), (x2, y2)), ...]
|
||||||
|
established_lines = []
|
||||||
|
|
||||||
|
def do_intersect(p1, p2, p3, p4):
|
||||||
|
"""
|
||||||
|
检测线段 (p1, p2) 和 (p3, p4) 是否严格相交 (不包括端点接触)
|
||||||
|
"""
|
||||||
|
# 检查是否共享端点
|
||||||
|
if (p1[0]==p3[0] and p1[1]==p3[1]) or (p1[0]==p4[0] and p1[1]==p4[1]) or \
|
||||||
|
(p2[0]==p3[0] and p2[1]==p3[1]) or (p2[0]==p4[0] and p2[1]==p4[1]):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ccw(A, B, C):
|
||||||
|
# 向量叉积
|
||||||
|
return (C[1]-A[1]) * (B[0]-A[0]) - (B[1]-A[1]) * (C[0]-A[0])
|
||||||
|
|
||||||
|
# 如果跨立实验符号相反,则相交
|
||||||
|
d1 = ccw(p1, p2, p3)
|
||||||
|
d2 = ccw(p1, p2, p4)
|
||||||
|
d3 = ccw(p3, p4, p1)
|
||||||
|
d4 = ccw(p3, p4, p2)
|
||||||
|
|
||||||
|
# 严格相交判断 (忽略共线重叠的情况,视为不交叉)
|
||||||
|
if ((d1 > 1e-9 and d2 < -1e-9) or (d1 < -1e-9 and d2 > 1e-9)) and \
|
||||||
|
((d3 > 1e-9 and d4 < -1e-9) or (d3 < -1e-9 and d4 > 1e-9)):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 3. 迭代优化
|
||||||
|
while True:
|
||||||
|
# 预先收集当前所有组件的 Gate Edges (连接升压站的线段)
|
||||||
|
# 格式: {cid: (gate_node_coord, substation_coord)}
|
||||||
|
current_gate_lines = {}
|
||||||
|
sub_coord_tuple = (sx, sy)
|
||||||
|
for cid, data in components.items():
|
||||||
|
gate_idx = data['gate_node']
|
||||||
|
current_gate_lines[cid] = (coords[gate_idx], sub_coord_tuple)
|
||||||
|
|
||||||
|
# 收集所有候选移动: (tradeoff, u, v, cid_u, cid_v)
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
# 建立 node_to_comp_id 映射以便快速查找
|
||||||
|
node_to_comp_id = {}
|
||||||
|
for cid, data in components.items():
|
||||||
|
for member in data['members']:
|
||||||
|
node_to_comp_id[member] = cid
|
||||||
|
|
||||||
|
# 遍历所有边 (i, j)
|
||||||
|
for i in range(n_turbines):
|
||||||
|
cid_i = node_to_comp_id[i]
|
||||||
|
gate_cost_i = components[cid_i]['gate_cost']
|
||||||
|
|
||||||
|
for j in range(n_turbines):
|
||||||
|
if i == j: continue
|
||||||
|
|
||||||
|
cid_j = node_to_comp_id[j]
|
||||||
|
|
||||||
|
# 必须是不同组件
|
||||||
|
if cid_i == cid_j:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查容量约束
|
||||||
|
if components[cid_i]['total_power'] + components[cid_j]['total_power'] > max_capacity_mw:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算 Tradeoff
|
||||||
|
dist_ij = dist_matrix[i, j]
|
||||||
|
tradeoff = dist_ij - gate_cost_i
|
||||||
|
|
||||||
|
# 只有当 tradeoff < 0 时,合并才是有益的
|
||||||
|
if tradeoff < -1e-9:
|
||||||
|
candidates.append((tradeoff, i, j, cid_i, cid_j))
|
||||||
|
|
||||||
|
# 按 tradeoff 排序 (从小到大,越小越好)
|
||||||
|
candidates.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
best_move = None
|
||||||
|
|
||||||
|
# 延迟检测: 从最好的开始检查交叉
|
||||||
|
for cand in candidates:
|
||||||
|
tradeoff, u, v, cid_u, cid_v = cand
|
||||||
|
|
||||||
|
p_u = coords[u]
|
||||||
|
p_v = coords[v]
|
||||||
|
|
||||||
|
# 快速包围盒测试 (AABB) 准备
|
||||||
|
min_x_uv, max_x_uv = min(p_u[0], p_v[0]), max(p_u[0], p_v[0])
|
||||||
|
min_y_uv, max_y_uv = min(p_u[1], p_v[1]), max(p_u[1], p_v[1])
|
||||||
|
|
||||||
|
is_crossing = False
|
||||||
|
|
||||||
|
# 1. 检查与已固定的内部边的交叉
|
||||||
|
for line in established_lines:
|
||||||
|
p_a, p_b = line[0], line[1]
|
||||||
|
|
||||||
|
if max(p_a[0], p_b[0]) < min_x_uv or min(p_a[0], p_b[0]) > max_x_uv or \
|
||||||
|
max(p_a[1], p_b[1]) < min_y_uv or min(p_a[1], p_b[1]) > max_y_uv:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if do_intersect(p_u, p_v, p_a, p_b):
|
||||||
|
is_crossing = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_crossing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2. 检查与所有活跃 Gate Edges 的交叉 (排除被移除的那个)
|
||||||
|
# 正在合并 cid_u -> cid_v,意味着 cid_u 的 Gate 将被移除。
|
||||||
|
# 但 cid_v 的 Gate 以及其他所有组件的 Gate 仍然存在。
|
||||||
|
for cid, gate_line in current_gate_lines.items():
|
||||||
|
if cid == cid_u:
|
||||||
|
continue # 这个 Gate 即将移除,不构成障碍
|
||||||
|
|
||||||
|
p_a, p_b = gate_line[0], gate_line[1]
|
||||||
|
|
||||||
|
if max(p_a[0], p_b[0]) < min_x_uv or min(p_a[0], p_b[0]) > max_x_uv or \
|
||||||
|
max(p_a[1], p_b[1]) < min_y_uv or min(p_a[1], p_b[1]) > max_y_uv:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if do_intersect(p_u, p_v, p_a, p_b):
|
||||||
|
is_crossing = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not is_crossing:
|
||||||
|
best_move = (u, v, cid_u, cid_v)
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果没有找到有益的合并,或者所有可行合并都会增加成本,则停止
|
||||||
|
if best_move is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 执行合并
|
||||||
|
u, v, cid_u, cid_v = best_move
|
||||||
|
|
||||||
|
# 将 cid_u 并入 cid_v
|
||||||
|
# 1. 记录新边
|
||||||
|
established_edges.append((u, v, dist_matrix[u, v]))
|
||||||
|
established_lines.append((coords[u], coords[v]))
|
||||||
|
|
||||||
|
# 2. 更新组件信息
|
||||||
|
comp_u = components[cid_u]
|
||||||
|
comp_v = components[cid_v]
|
||||||
|
|
||||||
|
# 合并成员
|
||||||
|
comp_v['members'].update(comp_u['members'])
|
||||||
|
comp_v['total_power'] += comp_u['total_power']
|
||||||
|
|
||||||
|
# Gate 节点和 Cost 保持 cid_v 的不变
|
||||||
|
# (因为我们将 U 接到了 V 上,U 的原 gate 被移除,V 的 gate 仍是通往升压站的路径)
|
||||||
|
|
||||||
|
# 3. 删除旧组件
|
||||||
|
del components[cid_u]
|
||||||
|
|
||||||
|
# 4. 构建最终结果
|
||||||
|
connections = []
|
||||||
|
|
||||||
|
# 添加内部边 (风机间)
|
||||||
|
for u, v, length in established_edges:
|
||||||
|
source = f'turbine_{u}'
|
||||||
|
target = f'turbine_{v}'
|
||||||
|
connections.append((source, target, length))
|
||||||
|
|
||||||
|
# 添加 Gate 边 (风机到升压站)
|
||||||
|
# 此时 components 中剩下的每个组件都有一个 gate_node 连接到 Substation
|
||||||
|
cluster_mapping = {} # node_id -> cluster_id (0..N-1)
|
||||||
|
|
||||||
|
for idx, (cid, data) in enumerate(components.items()):
|
||||||
|
gate_node = data['gate_node']
|
||||||
|
gate_cost = data['gate_cost']
|
||||||
|
|
||||||
|
connections.append((f'turbine_{gate_node}', 'substation', gate_cost))
|
||||||
|
|
||||||
|
# 记录 cluster id
|
||||||
|
for member in data['members']:
|
||||||
|
cluster_mapping[member] = idx
|
||||||
|
|
||||||
|
# 更新 turbines DataFrame
|
||||||
|
turbines_with_cluster = turbines_df.copy()
|
||||||
|
turbines_with_cluster['cluster'] = turbines_with_cluster['id'].map(cluster_mapping)
|
||||||
|
|
||||||
|
return connections, turbines_with_cluster
|
||||||
@@ -11,7 +11,8 @@ def create_template():
|
|||||||
'ID': 'Sub1',
|
'ID': 'Sub1',
|
||||||
'X': 4000,
|
'X': 4000,
|
||||||
'Y': -800,
|
'Y': -800,
|
||||||
'Power': 0
|
'Power': 0,
|
||||||
|
'PlatformHeight': 0
|
||||||
})
|
})
|
||||||
|
|
||||||
# Add Turbines (Grid layout)
|
# Add Turbines (Grid layout)
|
||||||
@@ -29,15 +30,32 @@ def create_template():
|
|||||||
'ID': i,
|
'ID': i,
|
||||||
'X': x,
|
'X': x,
|
||||||
'Y': y,
|
'Y': y,
|
||||||
'Power': np.random.uniform(6.0, 10.0)
|
'Power': np.random.uniform(6.0, 10.0),
|
||||||
|
'PlatformHeight': 0
|
||||||
})
|
})
|
||||||
|
|
||||||
df = pd.DataFrame(data)
|
df = pd.DataFrame(data)
|
||||||
|
|
||||||
|
# Create Cable data
|
||||||
|
cable_data = [
|
||||||
|
{'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)
|
||||||
|
|
||||||
# Save to Excel
|
# Save to Excel
|
||||||
output_file = 'coordinates.xlsx'
|
output_file = 'coordinates.xlsx'
|
||||||
df.to_excel(output_file, index=False)
|
with pd.ExcelWriter(output_file) as writer:
|
||||||
print(f"Created sample file: {output_file}")
|
df.to_excel(writer, sheet_name='Coordinates', index=False)
|
||||||
|
df_cables.to_excel(writer, sheet_name='Cables', index=False)
|
||||||
|
print(f"Created sample file: {output_file} with sheets 'Coordinates' and 'Cables'")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
create_template()
|
create_template()
|
||||||
854
main.py
854
main.py
@@ -7,11 +7,18 @@ from sklearn.cluster import KMeans
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import math
|
import math
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from esau_williams import design_with_esau_williams
|
||||||
|
|
||||||
# 设置matplotlib支持中文显示
|
# 设置matplotlib支持中文显示
|
||||||
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial']
|
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial']
|
||||||
plt.rcParams['axes.unicode_minus'] = False
|
plt.rcParams['axes.unicode_minus'] = False
|
||||||
|
|
||||||
|
# 常量定义
|
||||||
|
VOLTAGE_LEVEL = 66000 # 66kV
|
||||||
|
POWER_FACTOR = 0.95
|
||||||
|
|
||||||
# 1. 生成风电场数据(实际应用中替换为真实坐标)
|
# 1. 生成风电场数据(实际应用中替换为真实坐标)
|
||||||
def generate_wind_farm_data(n_turbines=30, seed=42, layout='random', spacing=800):
|
def generate_wind_farm_data(n_turbines=30, seed=42, layout='random', spacing=800):
|
||||||
"""
|
"""
|
||||||
@@ -61,6 +68,7 @@ def generate_wind_farm_data(n_turbines=30, seed=42, layout='random', spacing=800
|
|||||||
'x': x_coords,
|
'x': x_coords,
|
||||||
'y': y_coords,
|
'y': y_coords,
|
||||||
'power': power_ratings,
|
'power': power_ratings,
|
||||||
|
'platform_height': np.zeros(n_turbines),
|
||||||
'cumulative_power': np.zeros(n_turbines) # 用于后续计算
|
'cumulative_power': np.zeros(n_turbines) # 用于后续计算
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -69,11 +77,19 @@ def generate_wind_farm_data(n_turbines=30, seed=42, layout='random', spacing=800
|
|||||||
# 1.5 从Excel加载数据
|
# 1.5 从Excel加载数据
|
||||||
def load_data_from_excel(file_path):
|
def load_data_from_excel(file_path):
|
||||||
"""
|
"""
|
||||||
从Excel文件读取风机和升压站坐标
|
从Excel文件读取风机和升压站坐标,以及可选的电缆规格
|
||||||
Excel格式要求包含列: Type (Turbine/Substation), ID, X, Y, Power
|
Excel格式要求:
|
||||||
|
- Sheet 'Coordinates' (或第一个Sheet): Type (Turbine/Substation), ID, X, Y, Power
|
||||||
|
- Sheet 'Cables' (可选): CrossSection, Capacity, Resistance, Cost
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
df = pd.read_excel(file_path)
|
xl = pd.ExcelFile(file_path)
|
||||||
|
|
||||||
|
# 读取坐标数据
|
||||||
|
if 'Coordinates' in xl.sheet_names:
|
||||||
|
df = pd.read_excel(xl, 'Coordinates')
|
||||||
|
else:
|
||||||
|
df = pd.read_excel(xl, 0)
|
||||||
|
|
||||||
# 标准化列名(忽略大小写)
|
# 标准化列名(忽略大小写)
|
||||||
df.columns = [c.capitalize() for c in df.columns]
|
df.columns = [c.capitalize() for c in df.columns]
|
||||||
@@ -94,6 +110,15 @@ def load_data_from_excel(file_path):
|
|||||||
if len(turbines_df) == 0:
|
if len(turbines_df) == 0:
|
||||||
raise ValueError("未在文件中找到风机(Turbine)数据")
|
raise ValueError("未在文件中找到风机(Turbine)数据")
|
||||||
|
|
||||||
|
# 尝试获取平台高度列 (兼容不同命名)
|
||||||
|
platform_height_col = None
|
||||||
|
for col in turbines_df.columns:
|
||||||
|
if col.lower().replace(' ', '') == 'platformheight':
|
||||||
|
platform_height_col = col
|
||||||
|
break
|
||||||
|
|
||||||
|
platform_heights = turbines_df[platform_height_col].values if platform_height_col else np.zeros(len(turbines_df))
|
||||||
|
|
||||||
# 重置索引并整理格式
|
# 重置索引并整理格式
|
||||||
turbines = pd.DataFrame({
|
turbines = pd.DataFrame({
|
||||||
'id': range(len(turbines_df)),
|
'id': range(len(turbines_df)),
|
||||||
@@ -101,11 +126,41 @@ def load_data_from_excel(file_path):
|
|||||||
'x': turbines_df['X'].values,
|
'x': turbines_df['X'].values,
|
||||||
'y': turbines_df['Y'].values,
|
'y': turbines_df['Y'].values,
|
||||||
'power': turbines_df['Power'].values,
|
'power': turbines_df['Power'].values,
|
||||||
|
'platform_height': platform_heights,
|
||||||
'cumulative_power': np.zeros(len(turbines_df))
|
'cumulative_power': np.zeros(len(turbines_df))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 读取电缆数据 (如果存在)
|
||||||
|
cable_specs = None
|
||||||
|
if 'Cables' in xl.sheet_names:
|
||||||
|
cables_df = pd.read_excel(xl, 'Cables')
|
||||||
|
# 标准化列名
|
||||||
|
cables_df.columns = [c.replace(' ', '').capitalize() for c in cables_df.columns] # Handle 'Cross Section' vs 'CrossSection'
|
||||||
|
|
||||||
|
# 尝试匹配列
|
||||||
|
# 目标格式: (截面mm², 载流量A, 电阻Ω/km, 基准价格元/m, 是否可选)
|
||||||
|
specs = []
|
||||||
|
for _, row in cables_df.iterrows():
|
||||||
|
# 容错处理列名
|
||||||
|
section = row.get('Crosssection', row.get('Section', 0))
|
||||||
|
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, is_optional))
|
||||||
|
|
||||||
|
if specs:
|
||||||
|
specs.sort(key=lambda x: x[1]) # 按载流量排序
|
||||||
|
cable_specs = specs
|
||||||
|
|
||||||
print(f"成功加载: {len(turbines)} 台风机, {len(substation)} 座升压站")
|
print(f"成功加载: {len(turbines)} 台风机, {len(substation)} 座升压站")
|
||||||
return turbines, substation
|
if cable_specs:
|
||||||
|
print(f"成功加载: {len(cable_specs)} 种电缆规格")
|
||||||
|
|
||||||
|
return turbines, substation, cable_specs
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"读取Excel文件失败: {str(e)}")
|
print(f"读取Excel文件失败: {str(e)}")
|
||||||
@@ -220,20 +275,254 @@ def design_with_kmeans(turbines, substation, n_clusters=3):
|
|||||||
|
|
||||||
return cluster_connections + substation_connections, turbines
|
return cluster_connections + substation_connections, turbines
|
||||||
|
|
||||||
# 常量定义
|
# 3.5 带容量约束的扇区扫描算法 (Capacitated Sweep) - 基础版
|
||||||
VOLTAGE_LEVEL = 66000 # 66kV
|
def design_with_capacitated_sweep(turbines, substation, cable_specs=None):
|
||||||
POWER_FACTOR = 0.95
|
|
||||||
|
|
||||||
# 4. 电缆选型函数(简化版)
|
|
||||||
def select_cable(power, length, is_offshore=False):
|
|
||||||
"""
|
"""
|
||||||
基于功率和长度选择合适的电缆截面
|
使用带容量约束的扇区扫描算法设计集电线路 (基础版:单次扫描)
|
||||||
:param is_offshore: 是否为海上环境(成本更高)
|
原理:
|
||||||
|
1. 计算所有风机相对于升压站的角度。
|
||||||
|
2. 找到角度间隔最大的位置作为起始“切割线”,以避免切断密集的风机群。
|
||||||
|
3. 沿圆周方向扫描,贪婪地将风机加入当前回路,直到达到电缆容量上限。
|
||||||
|
4. 满载后开启新回路。
|
||||||
"""
|
"""
|
||||||
# 成本乘数:海缆材料+敷设成本通常是陆缆的4-6倍
|
# 1. 获取电缆最大容量
|
||||||
cost_multiplier = 5.0 if is_offshore else 1.0
|
max_mw = get_max_cable_capacity_mw(cable_specs)
|
||||||
|
# print(f"DEBUG: 扇区扫描算法启动 - 单回路容量限制: {max_mw:.2f} MW")
|
||||||
|
|
||||||
# 电缆规格库: (截面mm², 载流量A, 电阻Ω/km, 基准价格元/m)
|
substation_coord = substation[0]
|
||||||
|
|
||||||
|
# 2. 计算角度 (使用 arctan2 返回 -pi 到 pi)
|
||||||
|
# 避免直接修改原始DataFrame,使用副本
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 3. 寻找最佳起始角度 (最大角度间隙)
|
||||||
|
# 按角度排序
|
||||||
|
work_df = work_df.sort_values('angle').reset_index(drop=True) # 重置索引方便切片
|
||||||
|
|
||||||
|
angles = work_df['angle'].values
|
||||||
|
n = len(angles)
|
||||||
|
|
||||||
|
if n > 1:
|
||||||
|
# 计算相邻角度差
|
||||||
|
diffs = np.diff(angles)
|
||||||
|
# 计算首尾角度差 (跨越 ±pi 处)
|
||||||
|
wrap_diff = (2 * np.pi) - (angles[-1] - angles[0])
|
||||||
|
diffs = np.append(diffs, wrap_diff)
|
||||||
|
|
||||||
|
# 找到最大间隙的索引
|
||||||
|
max_gap_idx = np.argmax(diffs)
|
||||||
|
|
||||||
|
# 旋转数组,使最大间隙成为新的起点
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 4. 贪婪分组 (Capacity Constrained Clustering)
|
||||||
|
work_df['cluster'] = -1
|
||||||
|
cluster_id = 0
|
||||||
|
current_power = 0
|
||||||
|
current_indices = []
|
||||||
|
|
||||||
|
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
|
||||||
|
current_indices = []
|
||||||
|
|
||||||
|
current_indices.append(i)
|
||||||
|
current_power += p
|
||||||
|
|
||||||
|
if current_indices:
|
||||||
|
work_df.loc[current_indices, 'cluster'] = cluster_id
|
||||||
|
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连接
|
||||||
|
cluster_connections = []
|
||||||
|
substation_connections = []
|
||||||
|
n_clusters = cluster_id
|
||||||
|
|
||||||
|
for cid in range(n_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 = distance_matrix(coords, coords)
|
||||||
|
mst = minimum_spanning_tree(dist_matrix).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]}'
|
||||||
|
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()
|
||||||
|
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)。
|
||||||
|
|
||||||
|
基于提供的电缆规格列表,选取最大载流量,结合系统电压和功率因数计算理论最大传输功率。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
cable_specs (list, optional): 电缆规格列表。每个元素应包含 (截面积, 额定电流, 单价, 损耗系数)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
float: 最大功率承载能力 (MW)。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
Exception: 当未提供 cable_specs 时抛出,提示截面不满足。
|
||||||
|
"""
|
||||||
|
if cable_specs is None:
|
||||||
|
# Default cable specs if not provided (same as in evaluate_design)
|
||||||
cable_specs = [
|
cable_specs = [
|
||||||
(35, 150, 0.524, 80),
|
(35, 150, 0.524, 80),
|
||||||
(70, 215, 0.268, 120),
|
(70, 215, 0.268, 120),
|
||||||
@@ -242,44 +531,23 @@ def select_cable(power, length, is_offshore=False):
|
|||||||
(150, 330, 0.124, 220),
|
(150, 330, 0.124, 220),
|
||||||
(185, 370, 0.0991, 270),
|
(185, 370, 0.0991, 270),
|
||||||
(240, 425, 0.0754, 350),
|
(240, 425, 0.0754, 350),
|
||||||
(300, 500, 0.0601, 450), # 增加大截面适应海上大功率
|
(300, 500, 0.0601, 450),
|
||||||
(400, 580, 0.0470, 600)
|
(400, 580, 0.0470, 600)
|
||||||
]
|
]
|
||||||
|
|
||||||
# 估算电流
|
# 从所有电缆规格中找到最大的额定电流容量
|
||||||
# power是MW, 换算成W需要 * 1e6
|
max_current_capacity = max(spec[1] for spec in cable_specs)
|
||||||
current = (power * 1e6) / (np.sqrt(3) * VOLTAGE_LEVEL * POWER_FACTOR)
|
|
||||||
|
|
||||||
# 选择满足载流量的最小电缆
|
# 计算最大功率:P = √3 * U * I * cosφ
|
||||||
selected_spec = None
|
# 这里假设降额系数为 1 (不降额)
|
||||||
for spec in cable_specs:
|
max_current = max_current_capacity * 1
|
||||||
if current <= spec[1] * 0.8: # 80%负载率
|
|
||||||
selected_spec = spec
|
|
||||||
break
|
|
||||||
|
|
||||||
if selected_spec is None:
|
|
||||||
selected_spec = cable_specs[-1]
|
|
||||||
|
|
||||||
resistance = selected_spec[2] * length / 1000 # 电阻(Ω)
|
|
||||||
cost = selected_spec[3] * length * cost_multiplier # 电缆成本(含敷设)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'cross_section': selected_spec[0],
|
|
||||||
'current_capacity': selected_spec[1],
|
|
||||||
'resistance': resistance,
|
|
||||||
'cost': cost,
|
|
||||||
'current': current
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_max_cable_capacity_mw():
|
|
||||||
"""计算最大电缆(400mm2)能承载的最大功率(MW)"""
|
|
||||||
# 400mm2载流量580A
|
|
||||||
max_current = 580 * 0.8 # 80%降额
|
|
||||||
max_power_w = np.sqrt(3) * VOLTAGE_LEVEL * max_current * POWER_FACTOR
|
max_power_w = np.sqrt(3) * VOLTAGE_LEVEL * max_current * POWER_FACTOR
|
||||||
return max_power_w / 1e6 # MW
|
|
||||||
|
# 将单位从 W 转换为 MW
|
||||||
|
return max_power_w / 1e6
|
||||||
|
|
||||||
# 5. 计算集电线路方案成本
|
# 5. 计算集电线路方案成本
|
||||||
def evaluate_design(turbines, connections, substation, is_offshore=False):
|
def evaluate_design(turbines, connections, substation, cable_specs=None, is_offshore=False, method_name="Unknown Method"):
|
||||||
"""评估设计方案的总成本和损耗"""
|
"""评估设计方案的总成本和损耗"""
|
||||||
total_cost = 0
|
total_cost = 0
|
||||||
total_loss = 0
|
total_loss = 0
|
||||||
@@ -320,14 +588,31 @@ def evaluate_design(turbines, connections, substation, is_offshore=False):
|
|||||||
except nx.NetworkXNoPath:
|
except nx.NetworkXNoPath:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# DEBUG: 打印最大功率流
|
# DEBUG: 打印最大功率流 (不含升压站本身)
|
||||||
max_power = max(power_flow.values()) if power_flow else 0
|
node_powers = [v for k, v in power_flow.items() if k != 'substation']
|
||||||
print(f"DEBUG: 最大线路功率 = {max_power:.2f} MW")
|
max_power = max(node_powers) if node_powers else 0
|
||||||
|
print(f"DEBUG [{method_name}]: 最大线路功率 = {max_power:.2f} MW")
|
||||||
|
|
||||||
# 计算成本和损耗
|
# 计算成本和损耗
|
||||||
detailed_connections = []
|
detailed_connections = []
|
||||||
|
|
||||||
for source, target, length in connections:
|
for source, target, length in connections:
|
||||||
|
# Determine vertical length (PlatformHeight)
|
||||||
|
vertical_length = 0
|
||||||
|
|
||||||
|
if source.startswith('turbine_'):
|
||||||
|
tid = int(source.split('_')[1])
|
||||||
|
vertical_length += turbines.loc[tid, 'platform_height']
|
||||||
|
|
||||||
|
if target.startswith('turbine_'):
|
||||||
|
tid = int(target.split('_')[1])
|
||||||
|
vertical_length += turbines.loc[tid, 'platform_height']
|
||||||
|
|
||||||
|
# Calculate effective length with margin
|
||||||
|
# Total Length = (Horizontal Distance + Vertical Up/Down) * 1.03
|
||||||
|
horizontal_length = length
|
||||||
|
effective_length = (horizontal_length + vertical_length) * 1.03
|
||||||
|
|
||||||
# 确定该段线路承载的总功率
|
# 确定该段线路承载的总功率
|
||||||
if source.startswith('turbine_') and target.startswith('turbine_'):
|
if source.startswith('turbine_') and target.startswith('turbine_'):
|
||||||
# 风机间连接,取下游节点功率
|
# 风机间连接,取下游节点功率
|
||||||
@@ -345,13 +630,57 @@ def evaluate_design(turbines, connections, substation, is_offshore=False):
|
|||||||
power = 0
|
power = 0
|
||||||
|
|
||||||
# 电缆选型
|
# 电缆选型
|
||||||
cable = select_cable(power, length, is_offshore=is_offshore)
|
# 成本乘数:如果Excel中已包含敷设费用,则设为1.0
|
||||||
|
cost_multiplier = 1.0 if is_offshore else 1.0
|
||||||
|
|
||||||
|
# 默认电缆规格库 (如果未提供)
|
||||||
|
if cable_specs is None:
|
||||||
|
cable_specs_to_use = [
|
||||||
|
(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)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
cable_specs_to_use = cable_specs
|
||||||
|
|
||||||
|
# 估算电流
|
||||||
|
current = (power * 1e6) / (np.sqrt(3) * VOLTAGE_LEVEL * POWER_FACTOR)
|
||||||
|
|
||||||
|
# 选择满足载流量的最小电缆
|
||||||
|
selected_spec = None
|
||||||
|
for spec in cable_specs_to_use:
|
||||||
|
if current <= spec[1] * 1: # 100%负载率
|
||||||
|
selected_spec = spec
|
||||||
|
break
|
||||||
|
|
||||||
|
if selected_spec is None:
|
||||||
|
selected_spec = cable_specs_to_use[-1]
|
||||||
|
print(f"WARNING [{method_name}]: Current {current:.2f} A (Power: {power:.2f} MW) exceeds max cable capacity {selected_spec[1]} A!")
|
||||||
|
|
||||||
|
resistance = selected_spec[2] * effective_length / 1000 # 电阻(Ω)
|
||||||
|
cost = selected_spec[3] * effective_length * cost_multiplier # 电缆成本(含敷设)
|
||||||
|
|
||||||
|
cable = {
|
||||||
|
'cross_section': selected_spec[0],
|
||||||
|
'current_capacity': selected_spec[1],
|
||||||
|
'resistance': resistance,
|
||||||
|
'cost': cost,
|
||||||
|
'current': current
|
||||||
|
}
|
||||||
|
|
||||||
# 记录详细信息
|
# 记录详细信息
|
||||||
detailed_connections.append({
|
detailed_connections.append({
|
||||||
'source': source,
|
'source': source,
|
||||||
'target': target,
|
'target': target,
|
||||||
'length': length,
|
'horizontal_length': horizontal_length,
|
||||||
|
'vertical_length': vertical_length,
|
||||||
|
'length': effective_length, # effective length used for stats
|
||||||
'power': power,
|
'power': power,
|
||||||
'cable': cable
|
'cable': cable
|
||||||
})
|
})
|
||||||
@@ -385,15 +714,32 @@ def export_to_dxf(turbines, substation, connections_details, filename):
|
|||||||
# 1. 建立图层
|
# 1. 建立图层
|
||||||
doc.layers.add('Substation', color=1) # Red
|
doc.layers.add('Substation', color=1) # Red
|
||||||
doc.layers.add('Turbines', color=5) # Blue
|
doc.layers.add('Turbines', color=5) # Blue
|
||||||
doc.layers.add('Cable_35mm', color=3) # Green
|
|
||||||
doc.layers.add('Cable_70mm', color=130)
|
# 动态确定电缆颜色
|
||||||
doc.layers.add('Cable_95mm', color=150)
|
# 提取所有使用到的电缆截面
|
||||||
doc.layers.add('Cable_120mm', color=4) # Cyan
|
used_sections = sorted(list(set(conn['cable']['cross_section'] for conn in connections_details)))
|
||||||
doc.layers.add('Cable_150mm', color=6) # Magenta
|
|
||||||
doc.layers.add('Cable_185mm', color=30) # Orange
|
# 定义颜色映射规则 (AutoCAD Color Index)
|
||||||
doc.layers.add('Cable_240mm', color=1) # Red
|
# 1=Red, 2=Yellow, 3=Green, 4=Cyan, 5=Blue, 6=Magenta
|
||||||
doc.layers.add('Cable_300mm', color=250)
|
color_rank = [
|
||||||
doc.layers.add('Cable_400mm', color=7) # White/Black
|
2, # 1st (Smallest): Yellow
|
||||||
|
3, # 2nd: Green
|
||||||
|
1, # 3rd: Red
|
||||||
|
5, # 4th: Blue
|
||||||
|
6 # 5th: Magenta
|
||||||
|
]
|
||||||
|
default_color = 3 # Others: Green
|
||||||
|
|
||||||
|
# 创建电缆图层
|
||||||
|
for i, section in enumerate(used_sections):
|
||||||
|
if i < len(color_rank):
|
||||||
|
c = color_rank[i]
|
||||||
|
else:
|
||||||
|
c = default_color
|
||||||
|
|
||||||
|
layer_name = f'Cable_{section}mm'
|
||||||
|
if layer_name not in doc.layers:
|
||||||
|
doc.layers.add(layer_name, color=c)
|
||||||
|
|
||||||
# 2. 绘制升压站
|
# 2. 绘制升压站
|
||||||
sx, sy = substation[0, 0], substation[0, 1]
|
sx, sy = substation[0, 0], substation[0, 1]
|
||||||
@@ -418,7 +764,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:
|
||||||
@@ -436,8 +781,8 @@ def export_to_dxf(turbines, substation, connections_details, filename):
|
|||||||
if layer_name not in doc.layers:
|
if layer_name not in doc.layers:
|
||||||
doc.layers.add(layer_name)
|
doc.layers.add(layer_name)
|
||||||
|
|
||||||
# 绘制直线
|
# 绘制二维多段线
|
||||||
msp.add_line(p1, p2, dxfattribs={'layer': layer_name})
|
msp.add_lwpolyline([p1, p2], dxfattribs={'layer': layer_name})
|
||||||
|
|
||||||
# 添加电缆型号文字(可选,在线的中点)
|
# 添加电缆型号文字(可选,在线的中点)
|
||||||
# mid_x = (p1[0] + p2[0]) / 2
|
# mid_x = (p1[0] + p2[0]) / 2
|
||||||
@@ -450,6 +795,103 @@ def export_to_dxf(turbines, substation, connections_details, filename):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"导出DXF失败: {e}")
|
print(f"导出DXF失败: {e}")
|
||||||
|
|
||||||
|
# 6.6 导出Excel报表
|
||||||
|
def export_to_excel(connections_details, filename):
|
||||||
|
"""
|
||||||
|
将设计方案详情导出为Excel文件
|
||||||
|
:param connections_details: evaluate_design返回的'details'列表
|
||||||
|
"""
|
||||||
|
data = []
|
||||||
|
for conn in connections_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)
|
||||||
|
|
||||||
|
# 汇总统计
|
||||||
|
summary = {
|
||||||
|
'Total Cost (¥)': df['Cost (¥)'].sum(),
|
||||||
|
'Total Effective Length (m)': df['Effective Length (m)'].sum(),
|
||||||
|
'Total Vertical Length (m)': df['Vertical Length (m)'].sum()
|
||||||
|
}
|
||||||
|
summary_df = pd.DataFrame([summary])
|
||||||
|
|
||||||
|
try:
|
||||||
|
with pd.ExcelWriter(filename) as writer:
|
||||||
|
df.to_excel(writer, sheet_name='Cable Schedule', index=False)
|
||||||
|
summary_df.to_excel(writer, sheet_name='Summary', index=False)
|
||||||
|
print(f"成功导出Excel文件: {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. 可视化函数
|
# 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):
|
||||||
"""可视化集电线路设计方案"""
|
"""可视化集电线路设计方案"""
|
||||||
@@ -562,96 +1004,260 @@ def visualize_design(turbines, substation, connections, title, ax=None, show_cos
|
|||||||
return ax
|
return ax
|
||||||
|
|
||||||
# 7. 主函数:比较两种设计方法
|
# 7. 主函数:比较两种设计方法
|
||||||
def compare_design_methods(excel_path=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: 可选,手动指定簇的数量
|
||||||
"""
|
"""
|
||||||
|
cable_specs = None
|
||||||
if excel_path:
|
if excel_path:
|
||||||
print(f"正在从 {excel_path} 读取坐标数据...")
|
print(f"正在从 {excel_path} 读取坐标数据...")
|
||||||
try:
|
try:
|
||||||
turbines, substation = load_data_from_excel(excel_path)
|
turbines, substation, cable_specs = load_data_from_excel(excel_path)
|
||||||
scenario_title = "Offshore Wind Farm (Imported Data)"
|
scenario_title = "Offshore Wind Farm (Imported Data)"
|
||||||
except Exception:
|
except Exception:
|
||||||
print("回退到自动生成数据模式...")
|
print("回退到自动生成数据模式...")
|
||||||
return compare_design_methods(excel_path=None)
|
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, is_offshore=is_offshore)
|
mst_evaluation = evaluate_design(turbines, mst_connections, substation, cable_specs=specs_1, is_offshore=is_offshore, method_name="MST Method")
|
||||||
|
|
||||||
# 方法2:K-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()
|
|
||||||
min_clusters_needed = int(np.ceil(total_power / max_cable_mw))
|
|
||||||
|
|
||||||
# 增加一定的安全裕度 (1.2倍) 并确保至少有一定数量的簇
|
# 绘制 MST
|
||||||
n_clusters = max(int(min_clusters_needed * 1.2), 4)
|
|
||||||
if len(turbines) < n_clusters: # 避免簇数多于风机数
|
|
||||||
n_clusters = len(turbines)
|
|
||||||
|
|
||||||
print(f"系统设计参数: 总功率 {total_power:.1f} MW, 单回路最大容量 {max_cable_mw:.1f} MW")
|
|
||||||
print(f"计算建议回路数(簇数): {n_clusters} (最小需求 {min_clusters_needed})")
|
|
||||||
|
|
||||||
kmeans_connections, clustered_turbines = design_with_kmeans(turbines.copy(), substation, n_clusters=n_clusters)
|
|
||||||
kmeans_evaluation = evaluate_design(turbines, kmeans_connections, substation, is_offshore=is_offshore)
|
|
||||||
|
|
||||||
# 创建结果比较
|
|
||||||
results = {
|
|
||||||
'MST Method': mst_evaluation,
|
|
||||||
'K-means Method': 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方法
|
print(f"\n===== 开始比较电缆方案 =====")
|
||||||
visualize_design(clustered_turbines, substation, kmeans_evaluation['details'],
|
|
||||||
f"Sector Clustering (Angular) ({n_clusters} clusters) - {scenario_title}\nTotal Cost: ¥{kmeans_evaluation['total_cost']/10000:.2f}万\nTotal Loss: {kmeans_evaluation['total_loss']:.2f} kW",
|
best_cost = float('inf')
|
||||||
ax=axes[1])
|
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:
|
||||||
|
print(" 无可用电缆,跳过。")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算参数
|
||||||
|
total_power = turbines['power'].sum()
|
||||||
|
max_cable_mw = get_max_cable_capacity_mw(cable_specs=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}。")
|
||||||
|
else:
|
||||||
|
min_needed = int(np.ceil(total_power / max_cable_mw))
|
||||||
|
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")
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
)
|
||||||
|
eval_base = evaluate_design(
|
||||||
|
turbines, conns_base, substation, cable_specs=current_specs,
|
||||||
|
is_offshore=is_offshore, method_name=base_name
|
||||||
|
)
|
||||||
|
|
||||||
|
comparison_results.append({
|
||||||
|
'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
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# --- Run 3: Esau-Williams Algorithm ---
|
||||||
|
ew_name = f"{name} (Esau-Williams)"
|
||||||
|
conns_ew, turbines_ew = design_with_esau_williams(
|
||||||
|
turbines.copy(), substation, max_cable_mw
|
||||||
|
)
|
||||||
|
eval_ew = evaluate_design(
|
||||||
|
turbines, conns_ew, substation, cable_specs=current_specs,
|
||||||
|
is_offshore=is_offshore, method_name=ew_name
|
||||||
|
)
|
||||||
|
|
||||||
|
comparison_results.append({
|
||||||
|
'name': ew_name,
|
||||||
|
'cost': eval_ew['total_cost'],
|
||||||
|
'loss': eval_ew['total_loss'],
|
||||||
|
'eval': eval_ew,
|
||||||
|
'turbines': turbines_ew,
|
||||||
|
'specs': current_specs
|
||||||
|
})
|
||||||
|
print(f" [Esau-Williams] Cost: ¥{eval_ew['total_cost']:,.2f} | Loss: {eval_ew['total_loss']:.2f} kW")
|
||||||
|
|
||||||
|
# 记录最佳
|
||||||
|
if eval_rot['total_cost'] < best_cost:
|
||||||
|
best_cost = eval_rot['total_cost']
|
||||||
|
|
||||||
|
if eval_ew['total_cost'] < best_cost:
|
||||||
|
best_cost = eval_ew['total_cost']
|
||||||
|
# best_result 不再需要单独维护,最后遍历 comparison_results 即可
|
||||||
|
|
||||||
|
if eval_base['total_cost'] < best_cost:
|
||||||
|
best_cost = eval_base['total_cost']
|
||||||
|
|
||||||
|
# 可视化 (只画 Base 版本)
|
||||||
|
ax_idx = i + 1
|
||||||
|
if ax_idx < 4:
|
||||||
|
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()
|
plt.tight_layout()
|
||||||
output_filename = 'wind_farm_design_imported.png' if excel_path else 'offshore_wind_farm_design.png'
|
output_filename = 'wind_farm_design_comparison.png'
|
||||||
plt.savefig(output_filename, dpi=300)
|
plt.savefig(output_filename, dpi=300)
|
||||||
|
print(f"\n比较图(Base版)已保存至: {output_filename}")
|
||||||
|
|
||||||
# 导出DXF
|
# 准备文件路径
|
||||||
|
if excel_path:
|
||||||
|
base_name = os.path.splitext(os.path.basename(excel_path))[0]
|
||||||
|
dir_name = os.path.dirname(excel_path)
|
||||||
|
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'
|
dxf_filename = 'wind_farm_design.dxf'
|
||||||
# 默认导出更优的方案(通常K-means扇区聚类在海上更合理,或者成本更低者)
|
excel_out_filename = 'wind_farm_design.xlsx'
|
||||||
# 这里我们导出Sector Clustering的结果
|
|
||||||
export_to_dxf(clustered_turbines, substation, kmeans_evaluation['details'], dxf_filename)
|
|
||||||
|
|
||||||
plt.show()
|
# 导出所有方案到同一个 Excel
|
||||||
|
if comparison_results:
|
||||||
|
export_all_scenarios_to_excel(comparison_results, excel_out_filename)
|
||||||
|
|
||||||
# 打印详细结果
|
# 交互式选择导出 DXF
|
||||||
print(f"\n===== 海上风电场设计方案比较 ({'导入数据' if excel_path else '自动生成'}) =====")
|
print("\n===== 方案选择 =====")
|
||||||
for method, eval_data in results.items():
|
best_idx = 0
|
||||||
print(f"\n{method}:")
|
for i, res in enumerate(comparison_results):
|
||||||
print(f" 总成本: ¥{eval_data['total_cost']:,.2f} ({eval_data['total_cost']/10000:.2f}万元)")
|
if res['cost'] < comparison_results[best_idx]['cost']:
|
||||||
print(f" 预估总损耗: {eval_data['total_loss']:.2f} kW")
|
best_idx = i
|
||||||
print(f" 连接数量: {eval_data['num_connections']}")
|
print(f" {i+1}. {res['name']} - Cost: ¥{res['cost']:,.2f}")
|
||||||
|
|
||||||
return results
|
print(f"推荐方案: {comparison_results[best_idx]['name']} (默认)")
|
||||||
|
|
||||||
|
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. 执行比较
|
# 8. 执行比较
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import os
|
# 解析命令行参数
|
||||||
# 检查是否存在 coordinates.xlsx,存在则优先使用
|
parser = argparse.ArgumentParser(description='Wind Farm Collector System Design')
|
||||||
default_excel = 'coordinates.xlsx'
|
parser.add_argument('--excel', help='Path to the Excel coordinates file', default=None)
|
||||||
if os.path.exists(default_excel):
|
parser.add_argument('--clusters', type=int, help='Specify the number of clusters (circuits) manually', default=None)
|
||||||
results = compare_design_methods(excel_path=default_excel)
|
args = parser.parse_args()
|
||||||
else:
|
|
||||||
results = compare_design_methods()
|
# 3. 运行比较
|
||||||
|
# 如果没有提供excel文件,将自动回退到生成数据模式
|
||||||
|
compare_design_methods(args.excel, n_clusters_override=args.clusters)
|
||||||
|
|||||||
61
project_context.md
Normal file
61
project_context.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Project Context: Wind Farm Layout Optimization
|
||||||
|
|
||||||
|
**Last Updated:** 2025-12-30
|
||||||
|
**Project Path:** `D:\code\windfarm`
|
||||||
|
**Current Goal:** Optimize offshore wind farm cable layout using MST and K-means algorithms, with realistic constraints and CAD export.
|
||||||
|
|
||||||
|
## 1. System Overview
|
||||||
|
The system simulates and designs the collection system (inter-array cables) for an offshore wind farm.
|
||||||
|
It compares two main algorithms:
|
||||||
|
1. **MST (Minimum Spanning Tree)**: Global optimization of cable length. (Note: Often creates overloaded branches in large farms).
|
||||||
|
2. **Sector Clustering (K-means)**: Angular clustering to divide turbines into radial "strings" or "loops" feeding the substation. This is the preferred method for large offshore farms to ensure cable capacity constraints are met.
|
||||||
|
|
||||||
|
## 2. Key Implementations
|
||||||
|
|
||||||
|
### A. Data Handling
|
||||||
|
- **Generation**: Can generate random or grid layouts.
|
||||||
|
- **Import**: Supports reading coordinates from `coordinates.xlsx` (Columns: Type, ID, X, Y, Power).
|
||||||
|
- **Units**:
|
||||||
|
- Power in **MW** (input).
|
||||||
|
- Coordinates in **meters**.
|
||||||
|
- Voltage: **66 kV** (Code constant `VOLTAGE_LEVEL`).
|
||||||
|
|
||||||
|
### B. Algorithms
|
||||||
|
- **Angular K-means**:
|
||||||
|
- Uses `(cosθ, sinθ)` of the angle relative to substation for clustering.
|
||||||
|
- Eliminates cable crossings between sectors.
|
||||||
|
- **Dynamic Cluster Sizing**:
|
||||||
|
- Automatically calculates the required number of clusters (feeders) based on: `Total_Power / Max_Cable_Capacity`.
|
||||||
|
- Ensures no string exceeds the thermal limit of the largest available cable.
|
||||||
|
|
||||||
|
### C. Electrical Modeling
|
||||||
|
- **Cable Sizing**: Selects from standard cross-sections (35mm² to 400mm²).
|
||||||
|
- **Constraint**: Max cable capacity (400mm²) is approx. **50.4 MW** at 66kV/0.95PF.
|
||||||
|
- **Loss Calc**: $I^2 R$ losses.
|
||||||
|
|
||||||
|
### D. Visualization & Export
|
||||||
|
- **Matplotlib**: Shows layout with color-coded cables (Green=Thin -> Red=Thick).
|
||||||
|
- **DXF Export**: Uses `ezdxf` to generate `.dxf` files compatible with CAD.
|
||||||
|
- Layers: `Substation`, `Turbines`, `Cable_XXmm`.
|
||||||
|
- entities: Circles (Turbines), Polylines (Substation), Lines (Cables).
|
||||||
|
|
||||||
|
## 3. Critical Logic & Constants
|
||||||
|
- **Voltage**: 66,000 V
|
||||||
|
- **Power Factor**: 0.95
|
||||||
|
- **Max Current (400mm²)**: 580 A * 0.8 (derating) = 464 A.
|
||||||
|
- **Unit Conversion**: Critical fix applied to convert MW to Watts for current calculation (`power * 1e6`).
|
||||||
|
|
||||||
|
## 4. Current State & file Structure
|
||||||
|
- `main.py`: Core logic.
|
||||||
|
- `coordinates.xlsx`: Input data (if present).
|
||||||
|
- `wind_farm_design_imported.png`: Latest visualization.
|
||||||
|
- `wind_farm_design.dxf`: Latest CAD export.
|
||||||
|
|
||||||
|
## 5. Known Behaviors
|
||||||
|
- **MST Method**: Will report extremely high costs/losses for large farms because it creates a single tree structure that massively overloads the root cables. This is expected behavior (physically invalid but mathematically correct for unconstrained MST).
|
||||||
|
- **K-means Method**: Produces realistic, valid designs with appropriate cable tapering (e.g., 400mm² at root, 35mm² at leaves).
|
||||||
|
|
||||||
|
## 6. Future Improvements (Optional)
|
||||||
|
- **Obstacle Avoidance**: Currently assumes open ocean.
|
||||||
|
- **Loop Topology**: Current design is radial strings. Reliability could be improved with loop/ring structures.
|
||||||
|
- **Substation Placement Optimization**: Currently fixed or calculated as centroid.
|
||||||
Reference in New Issue
Block a user