Compare commits
54 Commits
2f70b2fc72
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b3ab83d91 | ||
|
|
68df76702e | ||
|
|
b3a4513f94 | ||
|
|
04a5e19451 | ||
|
|
ebd5883dbf | ||
|
|
41ac6f3963 | ||
|
|
09b2ada5df | ||
|
|
6441ddc059 | ||
|
|
2f095df12e | ||
|
|
a3837a6707 | ||
|
|
886fba4d15 | ||
|
|
397ca8847e | ||
|
|
6ad11a9b69 | ||
|
|
579f8866c4 | ||
|
|
4230d2221d | ||
|
|
46e929bfce | ||
|
|
f2a960e789 | ||
|
|
87cea6ed86 | ||
|
|
e0b5b0c3dc | ||
|
|
7aef58de1e | ||
|
|
45c99b41b3 | ||
|
|
837158270e | ||
|
|
61fa870778 | ||
|
|
c54ad369a4 | ||
|
|
86e0e21b58 | ||
|
|
60a9a57cee | ||
|
|
db6114ef57 | ||
|
|
67b1f55b92 | ||
|
|
b924f75add | ||
|
|
c6168afd1d | ||
|
|
9352005db3 | ||
|
|
15d8f4881d | ||
|
|
751bdef245 | ||
|
|
05ac7a3388 | ||
|
|
f28e087cd2 | ||
|
|
a5b46529da | ||
|
|
2ec763b86a | ||
|
|
a42a8ec7f1 | ||
|
|
dd7265ff4f | ||
|
|
3f73a9be26 | ||
|
|
369430aa67 | ||
|
|
00d480edbb | ||
|
|
06680a6e33 | ||
|
|
6f2f851a6e | ||
|
|
d563905f28 | ||
|
|
b5718a0cc2 | ||
|
|
6cac8806f0 | ||
|
|
34b0d70309 | ||
|
|
6454a2c01e | ||
|
|
2d50ab0df0 | ||
|
|
41e3cf355c | ||
|
|
e6d98297b1 | ||
|
|
e7e12745d1 | ||
|
|
4db9d138b8 |
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
# Build artifacts
|
||||
version_info.txt
|
||||
version.py
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
cpython-3.12.12-windows-x86_64-none
|
||||
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python Debugger: Current File with Arguments",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "main.py",
|
||||
"console": "integratedTerminal",
|
||||
"args": "--excel abc.xlsx"
|
||||
},
|
||||
]
|
||||
}
|
||||
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.
|
||||
240
IFLOW.md
Normal file
240
IFLOW.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# 海上风电场集电线路设计优化系统 - 项目上下文
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个用于设计和优化海上风电场集电系统拓扑的综合工具,专为海上能源业务开发部电气专业设计。该系统通过多种先进的拓扑优化算法(MST、旋转扫描法、Esau-Williams等),根据风机坐标、功率以及海缆规格,自动生成投资成本最低、损耗最优的设计方案。
|
||||
|
||||
### 核心功能
|
||||
- 🖥️ **图形化界面**:基于 NiceGUI 的现代化桌面应用,支持原生窗口模式
|
||||
- 🌊 **多种布局生成**:支持规则网格和随机分布布局的模拟数据生成
|
||||
- 🔌 **多算法优化**:
|
||||
- MST (Minimum Spanning Tree):无容量约束基准方案
|
||||
- Capacitated Sweep (Base):基础扇区扫描分组
|
||||
- Rotational Sweep:全局最优起始角度旋转扫描优化
|
||||
- Esau-Williams:经典启发式算法,在距离与容量间寻找最优平衡
|
||||
- ⚙️ **灵活参数配置**:通过 Excel 自定义系统电压、功率因数、电价及电缆规格
|
||||
- 📊 **智能方案对比**:自动运行三大场景(标准方案、含可选电缆方案、限制最大截面方案)
|
||||
- 📁 **多格式导出**:CAD图纸(.dxf)、Excel报告、压缩包
|
||||
|
||||
### 技术栈
|
||||
- **语言**:Python 3.12+
|
||||
- **GUI框架**:NiceGUI 3.4.1 + PyWebview 6.1
|
||||
- **核心库**:
|
||||
- numpy 2.4.0:数值计算
|
||||
- pandas 2.3.3:数据处理
|
||||
- matplotlib 3.10.8:可视化
|
||||
- scikit-learn 1.8.0:聚类算法
|
||||
- networkx 3.6.1:图算法
|
||||
- ezdxf 1.4.3:CAD导出
|
||||
- scipy 1.16.3:科学计算
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
D:\code\windfarm\
|
||||
├── main.py # 核心算法和业务逻辑(1388行)
|
||||
├── gui.py # NiceGUI图形界面(1067行)
|
||||
├── esau_williams.py # Esau-Williams算法实现(242行)
|
||||
├── generate_template.py # Excel模板生成器
|
||||
├── make_version.py # 版本号自动生成脚本
|
||||
├── pyproject.toml # 项目依赖配置
|
||||
├── Makefile # 构建脚本
|
||||
├── 使用说明/ # 中文操作手册和截图
|
||||
├── build/ # 构建输出目录
|
||||
└── dist/ # 打包输出目录
|
||||
```
|
||||
|
||||
## 构建和运行
|
||||
|
||||
### 环境配置
|
||||
项目使用 `uv` 作为包管理器,也支持 `pip`:
|
||||
|
||||
```bash
|
||||
# 使用 uv(推荐)
|
||||
uv sync
|
||||
|
||||
# 或使用 pip
|
||||
pip install -r requirements.txt # 如果有requirements.txt
|
||||
# 或手动安装依赖
|
||||
pip install numpy pandas matplotlib scikit-learn scipy networkx ezdxf nicegui openpyxl pywebview
|
||||
```
|
||||
|
||||
### 运行方式
|
||||
|
||||
#### 1. 图形化界面(推荐)
|
||||
```bash
|
||||
python gui.py
|
||||
```
|
||||
启动后,程序将弹出独立窗口,提供完整的交互式界面。
|
||||
|
||||
#### 2. 命令行模式
|
||||
```bash
|
||||
python main.py --excel your_data.xlsx
|
||||
```
|
||||
|
||||
### 构建可执行文件
|
||||
|
||||
使用 Makefile 进行构建:
|
||||
|
||||
```bash
|
||||
# 构建exe文件(自动生成版本号)
|
||||
make build
|
||||
|
||||
# 重新构建(先清理再构建)
|
||||
make rebuild
|
||||
|
||||
# 清理构建文件
|
||||
make clean
|
||||
|
||||
# 查看帮助
|
||||
make help
|
||||
```
|
||||
|
||||
构建过程:
|
||||
1. 运行 `make_version.py` 生成版本号
|
||||
2. 使用 `nicegui-pack` 打包为单文件exe
|
||||
3. 重命名输出文件包含版本号
|
||||
|
||||
构建输出位于 `dist/` 目录,文件名格式:`海上风电场集电线路设计优化系统_{VERSION}.exe`
|
||||
|
||||
## 输入数据规范
|
||||
|
||||
### Excel文件格式
|
||||
输入Excel文件应包含以下三个Sheet:
|
||||
|
||||
#### 1. Coordinates(坐标数据)- 必需
|
||||
| Type | ID | X | Y | Power | PlatformHeight |
|
||||
|------|----|---|---|-------|----------------|
|
||||
| Substation | Sub1 | 4000 | -800 | 0 | 0 |
|
||||
| Turbine | 1 | 0 | 0 | 8.0 | 25 |
|
||||
|
||||
- **Type**: `Substation` 或 `Turbine`
|
||||
- **X/Y**: 投影坐标(米),建议使用高斯投影坐标
|
||||
- **Power**: 功率(MW),升压站填0
|
||||
- **PlatformHeight**: 塔筒/平台高度(米)
|
||||
|
||||
#### 2. Cables(电缆规格)- 必需
|
||||
| CrossSection | Capacity | Resistance | Cost | Optional |
|
||||
|--------------|----------|------------|------|----------|
|
||||
| 35 | 150 | 0.524 | 80 | |
|
||||
| 400 | 580 | 0.0470 | 600 | Y |
|
||||
|
||||
- **CrossSection**: 导体截面(mm²)
|
||||
- **Capacity**: 额定载流量(A),需考虑降容系数
|
||||
- **Resistance**: 交流电阻(Ω/km)
|
||||
- **Cost**: 综合单价(元/m)
|
||||
- **Optional**: 可选标记(Y表示可选大截面电缆)
|
||||
|
||||
**重要规则**:
|
||||
- 电缆必须按截面从小到大排列
|
||||
- `Optional` 为 'Y' 的电缆最多只能有一条
|
||||
- 若存在可选电缆,它必须是列表中截面最大的一条
|
||||
|
||||
#### 3. Parameters(系统参数)- 必需
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Voltage (kV) / 电压 (kV) | 66 |
|
||||
| Power Factor / 功率因数 | 0.95 |
|
||||
| Electricity Price (元/kWh) / 电价 (元/kWh) | 0.4 |
|
||||
|
||||
## 核心算法说明
|
||||
|
||||
### 1. MST(最小生成树)
|
||||
- **原理**:基于 Kruskal 或 Prim 算法,寻找连接所有风机且总路径长度最短的树状结构
|
||||
- **特点**:不考虑电缆载流量限制,仅作为理论距离基准参考
|
||||
- **适用场景**:小规模风电场的理论分析
|
||||
|
||||
### 2. Capacitated Sweep(基础扇区扫描)
|
||||
- **原理**:以升压站为中心,将平面划分为扇区,按顺时针扫描风机
|
||||
- **特点**:计算速度快,拓扑结构简单清晰
|
||||
- **局限**:对起始扫描角度敏感,可能产生"长尾巴"连线
|
||||
|
||||
### 3. Rotational Sweep(旋转扫描优化)
|
||||
- **原理**:尝试 0° 到 360° 之间的所有起始扫描角度
|
||||
- **优势**:比基础扫描法节省 3%~8% 的线缆成本
|
||||
- **适用场景**:最接近人工精细化排布的自动化算法
|
||||
|
||||
### 4. Esau-Williams 启发式算法
|
||||
- **原理**:约束最小生成树(CMST)算法,迭代计算互联操作的成本节省
|
||||
- **优势**:能发现树状、多分叉等复杂但更经济的拓扑结构
|
||||
- **适用场景**:风机分布不规则、离岸距离较远或电缆造价极高的情况
|
||||
|
||||
## 方案场景说明
|
||||
|
||||
系统自动运行三种场景:
|
||||
|
||||
1. **Scenario 1 (Standard)**:仅使用非可选(标准)电缆进行优化
|
||||
2. **Scenario 2 (With Optional)**:包含标记为 'Y' 的大型电缆,适用于尝试增加单回路容量
|
||||
3. **Scenario 3 (No Max)**:排除最大截面电缆,测试电缆供应受限时的最优拓扑
|
||||
|
||||
## 输出文件说明
|
||||
|
||||
- **Excel报告**:`[文件名]_result.xlsx` - 包含所有方案总览及详细连接清单
|
||||
- **CAD图纸**:`design_[方案名].dxf` - 分层分色的拓扑图
|
||||
- **全部方案**:`[文件名]_result.zip` - 包含所有图纸及Excel报告
|
||||
|
||||
## 关键常量和配置
|
||||
|
||||
### 电气参数
|
||||
- **系统电压**:66,000 V (66kV)
|
||||
- **功率因数**:0.95
|
||||
- **电价**:0.4 元/kWh
|
||||
|
||||
### 电缆规格示例
|
||||
- 最小截面:35mm² (载流量150A)
|
||||
- 最大截面:400mm² (载流量580A)
|
||||
- 降容系数:0.8(实际载流量 = 额定载流量 × 0.8)
|
||||
|
||||
### 算法参数
|
||||
- 默认风机数量:30台
|
||||
- 默认布局:随机分布或网格
|
||||
- 默认间距:800米(网格布局)
|
||||
|
||||
## 开发约定
|
||||
|
||||
### 代码风格
|
||||
- 使用中文注释和文档字符串
|
||||
- 函数命名使用 snake_case
|
||||
- 类名使用 PascalCase
|
||||
- 常量使用 UPPER_CASE
|
||||
|
||||
### 版本管理
|
||||
- 版本号通过 `make_version.py` 自动生成
|
||||
- 版本号格式:v{major}.{minor}.{patch}
|
||||
- 版本号存储在 `version.py` 文件中
|
||||
|
||||
### 构建约定
|
||||
- 使用 `nicegui-pack` 进行打包
|
||||
- 单文件模式(--onefile)
|
||||
- 无窗口模式(--windowed)
|
||||
- 输出文件名包含版本号
|
||||
|
||||
### 测试约定
|
||||
- GUI测试使用 frontend-tester agent
|
||||
- Python代码测试使用 python-pro agent
|
||||
- 测试覆盖率要求:核心算法部分 > 80%
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: MST算法显示极高的成本和损耗?
|
||||
**A**: 这是预期行为。MST算法不考虑载流量约束,会产生单一树状结构导致根部电缆严重过载。这仅作为理论基准参考。
|
||||
|
||||
### Q2: 如何在CAD图纸中找到图形?
|
||||
**A**: 双击鼠标滚轮(Zoom Extents)全屏显示。风机坐标通常是大地坐标(数值很大),如果CAD当前视口在(0,0)附近,可能会找不到图形。
|
||||
|
||||
### Q3: 可选电缆的使用规则是什么?
|
||||
**A**:
|
||||
- 可选电缆(Optional='Y')最多只能有一条
|
||||
- 必须是列表中截面最大的电缆
|
||||
- 用于特定场景(如增加单回路容量)
|
||||
|
||||
## 技术支持
|
||||
|
||||
- **适用对象**:海上能源业务开发部 - 电气专业
|
||||
- **技术支持**:杜孟远
|
||||
- **文档版本**:v1.0
|
||||
- **编制日期**:2026年1月5日
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目仅供工程学习、研究和初步设计评估使用。详细计算应以专业设计院规范为准。
|
||||
34
Makefile
Normal file
34
Makefile
Normal file
@@ -0,0 +1,34 @@
|
||||
.PHONY: help clean build rebuild
|
||||
|
||||
# 默认目标
|
||||
help:
|
||||
@echo "海上风电场集电线路设计优化系统 - 构建脚本"
|
||||
@echo ""
|
||||
@echo "可用命令:"
|
||||
@echo " make build - 使用 .spec 文件生成单文件 exe 程序 (包含自动版本号生成)"
|
||||
@echo " make rebuild - 清理并重新构建"
|
||||
@echo " make clean - 清理编译生成的临时文件和缓存"
|
||||
@echo " make help - 显示此帮助信息"
|
||||
|
||||
# 生成单文件exe程序
|
||||
# 使用 nicegui-pack 打包
|
||||
build:
|
||||
@echo "正在生成版本信息..."
|
||||
uv run python make_version.py
|
||||
@echo "开始构建程序..."
|
||||
uv run nicegui-pack --onefile --windowed --name "海上风电场集电线路设计优化系统" --add-data "version.py:." gui.py
|
||||
@echo "正在重命名文件..."
|
||||
@uv run python -c "import os, shutil; from version import VERSION; src='dist/海上风电场集电线路设计优化系统.exe'; dst=f'dist/海上风电场集电线路设计优化系统_{VERSION}.exe'; shutil.move(src, dst); print(f'已重命名为: {dst}')"
|
||||
@echo "构建完成!"
|
||||
|
||||
# 清理编译生成的临时文件
|
||||
clean:
|
||||
@echo "正在清理临时文件..."
|
||||
@uv run python -c "import shutil, pathlib; [shutil.rmtree(p) for p in pathlib.Path('.').rglob('__pycache__')]; shutil.rmtree('build', ignore_errors=True); shutil.rmtree('dist', ignore_errors=True)"
|
||||
@echo "清理完成!"
|
||||
|
||||
nice:
|
||||
uv run nicegui-pack --onefile --name "海上风电场集电线路设计优化系统" gui.py --onefile --windowed
|
||||
|
||||
# 清理并重新构建
|
||||
rebuild: clean build
|
||||
155
README.md
155
README.md
@@ -1,110 +1,91 @@
|
||||
# 海上风电场集电线路设计优化工具
|
||||
# 海上风电场集电系统设计优化工具 (Wind Farm Collector System Optimizer)
|
||||
|
||||
## 项目简介
|
||||
一个用于设计和优化海上风电场集电系统拓扑的综合工具。支持多种先进算法,能够根据风机坐标、功率以及海缆规格,自动生成投资成本最低、损耗最小的设计方案。
|
||||
|
||||
这是一个用于海上风电场集电线路拓扑设计和优化的Python工具。它专注于解决大规模海上风电场的集电系统规划问题,通过算法比较不同设计方案的经济性和技术指标。
|
||||
## 🌟 主要功能
|
||||
|
||||
本项目特别针对**海上风电**场景进行了优化,考虑了海缆的高昂成本、大功率风机(6-10MW)以及严格的电缆载流量约束。
|
||||
- 🖥️ **原生桌面体验**:支持 Native 模式运行,提供类似本地应用的流畅体验(基于 NiceGUI & PyWebview)。
|
||||
- 🌊 **多种布局生成**:内置模拟数据生成器,支持规则网格和随机分布布局。
|
||||
- 🔌 **先进设计算法**:
|
||||
- **MST (Minimum Spanning Tree)**:无容量约束基准方案。
|
||||
- **Capacitated Sweep (Base)**:基础扇区扫描分组。
|
||||
- **Rotational Sweep**:全局最优起始角度旋转扫描优化。
|
||||
- **Esau-Williams**:经典启发式算法,在距离与容量间寻找最优平衡。
|
||||
- ⚙️ **灵活参数配置**:支持通过 Excel 自定义系统电压、功率因数及详细电缆规格。
|
||||
- 📊 **智能方案对比**:自动运行三大场景(标准方案、含可选电缆方案、限制最大截面方案)并对比结果。
|
||||
- 📁 **多格式导出**:
|
||||
- 自动生成 CAD 图纸 (`.dxf`),按电缆规格分层并着色。
|
||||
- 导出详细的 Excel 对比报告及单方案电缆清册。
|
||||
- 支持一键打包导出所有方案压缩包 (`.zip`)。
|
||||
|
||||
## 核心功能
|
||||
## 🛠️ 安装依赖
|
||||
|
||||
### 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) 进行依赖管理。
|
||||
|
||||
### 安装依赖
|
||||
本项目使用 `uv` 或 `pip` 管理环境。推荐安装依赖:
|
||||
|
||||
```bash
|
||||
# 使用 uv (推荐)
|
||||
uv add numpy pandas matplotlib scipy scikit-learn networkx ezdxf openpyxl
|
||||
|
||||
# 或使用 pip
|
||||
pip install numpy pandas matplotlib scipy scikit-learn networkx ezdxf openpyxl
|
||||
pip install numpy pandas matplotlib scikit-learn scipy networkx ezdxf nicegui openpyxl pywebview
|
||||
```
|
||||
*注:`pywebview` 用于支持原生窗口模式。*
|
||||
|
||||
## 使用方法
|
||||
## 🚀 使用方法
|
||||
|
||||
### 1. 运行主程序
|
||||
### 1. 启动图形化界面 (推荐)
|
||||
|
||||
运行以下命令启动应用,程序将自动弹出独立窗口:
|
||||
|
||||
```bash
|
||||
# 使用 uv
|
||||
uv run main.py
|
||||
|
||||
# 或直接运行
|
||||
python main.py
|
||||
python gui.py
|
||||
```
|
||||
|
||||
### 2. 数据输入模式
|
||||
**GUI 特性:**
|
||||
- **模板导出**:点击界面上的 "导出 Excel 模板" 按钮可获取标准输入格式文件。
|
||||
- **参数概览**:上传文件后自动解析并显示系统参数及电缆规格。
|
||||
- **交互式分析**:点击结果表格中的行,可实时查看对应方案的拓扑图。
|
||||
|
||||
程序会自动检测当前目录下是否存在 `coordinates.xlsx`:
|
||||
### 2. 命令行模式
|
||||
|
||||
- **存在**:优先读取 Excel 文件中的坐标数据进行计算。
|
||||
- **不存在**:自动生成 30 台风机的规则布局(Grid Layout)进行演示。
|
||||
|
||||
### 3. 结果输出
|
||||
|
||||
程序运行结束后会:
|
||||
1. 在终端打印详细的成本、损耗及电缆统计数据。
|
||||
2. 弹窗显示拓扑对比图,并保存为 `wind_farm_design_imported.png` (或 `offshore_...png`)。
|
||||
3. 生成 CAD 图纸文件 `wind_farm_design.dxf`。
|
||||
|
||||
## 关键参数说明
|
||||
|
||||
可以在 `main.py` 中调整以下核心常量以适配不同项目:
|
||||
|
||||
```python
|
||||
VOLTAGE_LEVEL = 66000 # 集电系统电压 (V)
|
||||
POWER_FACTOR = 0.95 # 功率因数
|
||||
cost_multiplier = 5.0 # 海缆相对于陆缆的成本倍数
|
||||
```bash
|
||||
python main.py --excel your_data.xlsx
|
||||
```
|
||||
|
||||
## 输出示例
|
||||
## 📝 输入数据规范 (Excel)
|
||||
|
||||
```text
|
||||
系统设计参数: 总功率 2000.0 MW, 单回路最大容量 50.4 MW
|
||||
计算建议回路数(簇数): 48 (最小需求 40)
|
||||
为了确保计算结果的准确性,输入 Excel 文件应包含以下 Sheet(推荐使用 GUI 导出模板):
|
||||
|
||||
[Sector Clustering] 电缆统计:
|
||||
70mm²: 48 条
|
||||
185mm²: 37 条
|
||||
400mm²: 40 条
|
||||
### 1. Coordinates (坐标)
|
||||
| Type | ID | X | Y | Power | PlatformHeight |
|
||||
|------|----|---|---|-------|----------------|
|
||||
| Substation | Sub1 | 4000 | -800 | 0 | 0 |
|
||||
| Turbine | 1 | 0 | 0 | 8.0 | 25 |
|
||||
| ... | ... | ... | ... | ... | ... |
|
||||
|
||||
成功导出DXF文件: wind_farm_design.dxf
|
||||
```
|
||||
### 2. Cables (电缆)
|
||||
**必须遵守以下规则:**
|
||||
- **单调递增性**:电缆必须按截面从小到大排列,且对应的额定载流量也必须严格递增。
|
||||
- **可选电缆规则**:
|
||||
- `Optional` 列标记为 'Y' 的电缆最多只能有一条。
|
||||
- 若存在可选电缆,它必须是列表中截面最大的一条。
|
||||
|
||||
## 许可证
|
||||
### 3. Parameters (参数) [可选]
|
||||
可自定义系统级参数,若不提供则使用默认值。
|
||||
|
||||
本项目仅供学习和研究使用。
|
||||
| Parameter | Value | 说明 |
|
||||
|-----------|-------|------|
|
||||
| Voltage (kV) | 66 | 系统电压,支持 `Voltage` 或 `System Voltage`。若 key 包含 `kV` 则自动 *1000。 |
|
||||
| Power Factor | 0.95 | 功率因数 (0-1)。 |
|
||||
|
||||
## 📈 场景说明 (Scenarios)
|
||||
|
||||
1. **Scenario 1 (Standard)**:仅使用非可选(标准)电缆进行优化。
|
||||
2. **Scenario 2 (With Optional)**:包含标记为 'Y' 的大型电缆,适用于尝试增加单回路容量的场景。
|
||||
3. **Scenario 3 (No Max)**:排除最大截面电缆,测试在电缆供应受限时的最优拓扑。
|
||||
|
||||
## 📂 输出文件说明
|
||||
|
||||
- **Excel 报告**:`[文件名]_result.xlsx` 包含所有方案的总览及详细连接清单。
|
||||
- **CAD 图纸**:`design_[方案名].dxf` 包含分层分色的拓扑图。
|
||||
- **全部方案**:`[文件名]_result.zip` 包含所有图纸及 Excel 报告。
|
||||
|
||||
## ⚖️ 许可证
|
||||
|
||||
本项目仅供工程学习、研究和初步设计评估使用。详细计算应以专业设计院规范为准。
|
||||
BIN
Z4(22MW)-4-9行-需重新布置集电海缆-换流站在西侧.xls
Normal file
BIN
Z4(22MW)-4-9行-需重新布置集电海缆-换流站在西侧.xls
Normal file
Binary file not shown.
87
build.spec
Normal file
87
build.spec
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
||||
|
||||
# 收集所有需要的数据文件
|
||||
datas = []
|
||||
datas += collect_data_files('matplotlib')
|
||||
datas += collect_data_files('nicegui')
|
||||
datas += collect_data_files('networkx')
|
||||
datas += collect_data_files('scipy')
|
||||
datas += collect_data_files('sklearn')
|
||||
datas += collect_data_files('pandas')
|
||||
|
||||
# 收集所有隐藏导入
|
||||
hiddenimports = []
|
||||
hiddenimports += collect_submodules('matplotlib')
|
||||
hiddenimports += collect_submodules('nicegui')
|
||||
hiddenimports += collect_submodules('networkx')
|
||||
hiddenimports += collect_submodules('scipy')
|
||||
hiddenimports += collect_submodules('sklearn')
|
||||
hiddenimports += collect_submodules('pandas')
|
||||
hiddenimports += collect_submodules('numpy')
|
||||
|
||||
# 添加特定的隐藏导入
|
||||
hiddenimports += [
|
||||
'matplotlib.backends.backend_qt5agg',
|
||||
'matplotlib.backends.backend_tkagg',
|
||||
'matplotlib.backends.backend_agg',
|
||||
'matplotlib.backends.backend_svg',
|
||||
'PIL._tkinter_finder',
|
||||
'openpyxl',
|
||||
'ezdxf',
|
||||
'scipy.spatial._qhull',
|
||||
'scipy.special._cdflib',
|
||||
'scipy.linalg.cython_lapack',
|
||||
'scipy.linalg.cython_blas',
|
||||
'sklearn.utils._cython_blas',
|
||||
'sklearn.neighbors._partition_nodes',
|
||||
'sklearn.tree._utils',
|
||||
'pandas._libs.tslibs.nattype',
|
||||
'pandas._libs.tslibs.np_datetime',
|
||||
'pandas._libs.skiplist',
|
||||
]
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['gui.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='海上风电场集电线路设计优化系统',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=None,
|
||||
)
|
||||
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
|
||||
193
ga.py
Normal file
193
ga.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.spatial import distance_matrix
|
||||
from scipy.sparse.csgraph import minimum_spanning_tree
|
||||
from collections import defaultdict
|
||||
import random
|
||||
|
||||
|
||||
def design_with_ga(
|
||||
turbines,
|
||||
substation,
|
||||
cable_specs=None,
|
||||
voltage=66000,
|
||||
power_factor=0.95,
|
||||
system_params=None,
|
||||
pop_size=50,
|
||||
generations=50,
|
||||
evaluate_func=None,
|
||||
total_invest_func=None,
|
||||
get_max_capacity_func=None,
|
||||
):
|
||||
"""
|
||||
使用遗传算法优化集电线路布局
|
||||
:param turbines: 风机DataFrame
|
||||
:param substation: 升压站坐标
|
||||
:param cable_specs: 电缆规格
|
||||
:param system_params: 系统参数(用于NPV计算)
|
||||
:param pop_size: 种群大小
|
||||
:param generations: 迭代代数
|
||||
:param evaluate_func: 评估函数
|
||||
:param total_invest_func: 总投资计算函数
|
||||
:param get_max_capacity_func: 获取最大容量函数
|
||||
:return: 连接列表和带有簇信息的turbines
|
||||
"""
|
||||
if get_max_capacity_func:
|
||||
max_mw = get_max_capacity_func(cable_specs, voltage, power_factor)
|
||||
else:
|
||||
max_mw = 100.0 # 默认值
|
||||
total_power = turbines["power"].sum()
|
||||
max_clusters = int(np.ceil(total_power / max_mw))
|
||||
n_turbines = len(turbines)
|
||||
|
||||
# 预计算距离矩阵
|
||||
all_coords = np.vstack([substation, turbines[["x", "y"]].values])
|
||||
dist_matrix_full = distance_matrix(all_coords, all_coords)
|
||||
|
||||
def fitness(chromosome):
|
||||
cluster_assign = chromosome
|
||||
clusters = defaultdict(list)
|
||||
for i, c in enumerate(cluster_assign):
|
||||
clusters[c].append(i)
|
||||
|
||||
connections = []
|
||||
for c, members in clusters.items():
|
||||
if len(members) == 0:
|
||||
continue
|
||||
coords = turbines.iloc[members][["x", "y"]].values
|
||||
if len(members) > 1:
|
||||
dm = distance_matrix(coords, coords)
|
||||
mst = minimum_spanning_tree(dm).toarray()
|
||||
for i in range(len(members)):
|
||||
for j in range(len(members)):
|
||||
if mst[i, j] > 0:
|
||||
connections.append(
|
||||
(
|
||||
f"turbine_{members[i]}",
|
||||
f"turbine_{members[j]}",
|
||||
mst[i, j],
|
||||
)
|
||||
)
|
||||
# 连接到升压站
|
||||
dists = [dist_matrix_full[0, m + 1] for m in members]
|
||||
closest = members[np.argmin(dists)]
|
||||
connections.append((f"turbine_{closest}", "substation", min(dists)))
|
||||
|
||||
eval_res = evaluate_func(
|
||||
turbines,
|
||||
connections,
|
||||
substation,
|
||||
cable_specs,
|
||||
is_offshore=False,
|
||||
method_name="GA",
|
||||
voltage=voltage,
|
||||
power_factor=power_factor,
|
||||
)
|
||||
if system_params and total_invest_func:
|
||||
res_list = total_invest_func(
|
||||
[
|
||||
{
|
||||
"cost": eval_res["total_cost"],
|
||||
"loss": eval_res["total_loss"],
|
||||
"eval": eval_res,
|
||||
}
|
||||
],
|
||||
system_params,
|
||||
)
|
||||
return res_list[0]["total_cost_npv"]
|
||||
return eval_res["total_cost"]
|
||||
|
||||
def init_individual():
|
||||
assign = np.zeros(n_turbines, dtype=int)
|
||||
cluster_powers = np.zeros(max_clusters)
|
||||
for i in range(n_turbines):
|
||||
p = turbines.iloc[i]["power"]
|
||||
possible = [
|
||||
c for c in range(max_clusters) if cluster_powers[c] + p <= max_mw
|
||||
]
|
||||
if possible:
|
||||
c = random.choice(possible)
|
||||
else:
|
||||
c = random.randint(0, max_clusters - 1)
|
||||
assign[i] = c
|
||||
cluster_powers[c] += p
|
||||
return assign.tolist()
|
||||
|
||||
population = [init_individual() for _ in range(pop_size)]
|
||||
best = None
|
||||
best_fitness = float("inf")
|
||||
|
||||
for gen in range(generations):
|
||||
fitnesses = [fitness(ind) for ind in population]
|
||||
min_fit = min(fitnesses)
|
||||
if min_fit < best_fitness:
|
||||
best_fitness = min_fit
|
||||
best = population[fitnesses.index(min_fit)].copy()
|
||||
|
||||
def tournament(size=3):
|
||||
candidates = random.sample(list(zip(population, fitnesses)), size)
|
||||
return min(candidates, key=lambda x: x[1])[0]
|
||||
|
||||
selected = [tournament() for _ in range(pop_size)]
|
||||
|
||||
new_pop = []
|
||||
for i in range(0, pop_size, 2):
|
||||
p1 = selected[i]
|
||||
p2 = selected[i + 1] if i + 1 < pop_size else selected[0]
|
||||
if random.random() < 0.8:
|
||||
point = random.randint(1, n_turbines - 1)
|
||||
child1 = p1[:point] + p2[point:]
|
||||
child2 = p2[:point] + p1[point:]
|
||||
else:
|
||||
child1, child2 = p1.copy(), p2.copy()
|
||||
new_pop.extend([child1, child2])
|
||||
|
||||
for ind in new_pop:
|
||||
if random.random() < 0.1:
|
||||
idx = random.randint(0, n_turbines - 1)
|
||||
old_c = ind[idx]
|
||||
new_c = random.randint(0, max_clusters - 1)
|
||||
ind[idx] = new_c
|
||||
cluster_powers = defaultdict(float)
|
||||
for j, c in enumerate(ind):
|
||||
cluster_powers[c] += turbines.iloc[j]["power"]
|
||||
if max(cluster_powers.values()) > max_mw:
|
||||
ind[idx] = max_clusters
|
||||
max_clusters += 1
|
||||
|
||||
elites = sorted(zip(population, fitnesses), key=lambda x: x[1])[
|
||||
: int(0.1 * pop_size)
|
||||
]
|
||||
new_pop[: len(elites)] = [e[0] for e in elites]
|
||||
population = new_pop[:pop_size]
|
||||
|
||||
# 解码最佳个体
|
||||
cluster_assign = best
|
||||
clusters = defaultdict(list)
|
||||
for i, c in enumerate(cluster_assign):
|
||||
clusters[c].append(i)
|
||||
|
||||
connections = []
|
||||
for c, members in clusters.items():
|
||||
if len(members) == 0:
|
||||
continue
|
||||
coords = turbines.iloc[members][["x", "y"]].values
|
||||
if len(members) > 1:
|
||||
dm = distance_matrix(coords, coords)
|
||||
mst = minimum_spanning_tree(dm).toarray()
|
||||
for i in range(len(members)):
|
||||
for j in range(len(members)):
|
||||
if mst[i, j] > 0:
|
||||
connections.append(
|
||||
(
|
||||
f"turbine_{members[i]}",
|
||||
f"turbine_{members[j]}",
|
||||
mst[i, j],
|
||||
)
|
||||
)
|
||||
dists = [dist_matrix_full[0, m + 1] for m in members]
|
||||
closest = members[np.argmin(dists)]
|
||||
connections.append((f"turbine_{closest}", "substation", min(dists)))
|
||||
|
||||
turbines["cluster"] = cluster_assign
|
||||
return connections, turbines
|
||||
@@ -1,7 +1,7 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
def create_template():
|
||||
def create_template(output_file='windfarm_template.xlsx'):
|
||||
# Create sample data similar to the internal generator
|
||||
data = []
|
||||
|
||||
@@ -11,7 +11,8 @@ def create_template():
|
||||
'ID': 'Sub1',
|
||||
'X': 4000,
|
||||
'Y': -800,
|
||||
'Power': 0
|
||||
'Power': 0,
|
||||
'PlatformHeight': 0
|
||||
})
|
||||
|
||||
# Add Turbines (Grid layout)
|
||||
@@ -29,15 +30,43 @@ def create_template():
|
||||
'ID': i,
|
||||
'X': x,
|
||||
'Y': y,
|
||||
'Power': np.random.uniform(6.0, 10.0)
|
||||
'Power': np.random.uniform(6.0, 10.0),
|
||||
'PlatformHeight': 0
|
||||
})
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Create Cable data
|
||||
cable_data = [
|
||||
{'CrossSection': 35, 'Capacity': 150, 'Resistance': 0.524, 'Cost': 8, 'Optional': ''},
|
||||
{'CrossSection': 70, 'Capacity': 215, 'Resistance': 0.268, 'Cost': 12, 'Optional': ''},
|
||||
{'CrossSection': 95, 'Capacity': 260, 'Resistance': 0.193, 'Cost': 15, 'Optional': ''},
|
||||
{'CrossSection': 120, 'Capacity': 295, 'Resistance': 0.153, 'Cost': 18, 'Optional': ''},
|
||||
{'CrossSection': 150, 'Capacity': 330, 'Resistance': 0.124, 'Cost': 22, 'Optional': ''},
|
||||
{'CrossSection': 185, 'Capacity': 370, 'Resistance': 0.0991, 'Cost': 27, 'Optional': ''},
|
||||
{'CrossSection': 240, 'Capacity': 425, 'Resistance': 0.0754, 'Cost': 35, 'Optional': ''},
|
||||
{'CrossSection': 300, 'Capacity': 500, 'Resistance': 0.0601, 'Cost': 45, 'Optional': ''},
|
||||
{'CrossSection': 400, 'Capacity': 580, 'Resistance': 0.0470, 'Cost': 60, 'Optional': ''}
|
||||
]
|
||||
df_cables = pd.DataFrame(cable_data)
|
||||
|
||||
# Create System Parameters data
|
||||
param_data = [
|
||||
{'Parameter': 'Voltage (kV) / 电压 (kV)', 'Value': 66},
|
||||
{'Parameter': 'Power Factor / 功率因数', 'Value': 0.95},
|
||||
{'Parameter': 'Electricity Price (元/kWh) / 电价 (元/kWh)', 'Value': 0.4},
|
||||
{'Parameter': 'Project Lifetime (years) / 工程运行期限/年', 'Value': 25},
|
||||
{'Parameter': 'Discount Rate (%) / 折现率%', 'Value': 8},
|
||||
{'Parameter': 'Annual Loss Hours (hours) / 年损耗小时数/小时', 'Value': 1400}
|
||||
]
|
||||
df_params = pd.DataFrame(param_data)
|
||||
|
||||
# Save to Excel
|
||||
output_file = 'coordinates.xlsx'
|
||||
df.to_excel(output_file, index=False)
|
||||
print(f"Created sample file: {output_file}")
|
||||
with pd.ExcelWriter(output_file) as writer:
|
||||
df.to_excel(writer, sheet_name='Coordinates', index=False)
|
||||
df_cables.to_excel(writer, sheet_name='Cables', index=False)
|
||||
df_params.to_excel(writer, sheet_name='Parameters', index=False)
|
||||
print(f"Created sample file: {output_file} with sheets 'Coordinates', 'Cables', and 'Parameters'")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_template()
|
||||
55
make_version.py
Normal file
55
make_version.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# make_version.py
|
||||
import datetime
|
||||
import os
|
||||
|
||||
def create_version_file():
|
||||
# 1. 生成版本号 (示例:使用 年.月.日.0)
|
||||
today = datetime.date.today()
|
||||
# 格式:(2026, 1, 5, 0)
|
||||
ver_tuple = (today.year, today.month, today.day, 0)
|
||||
ver_str = f"{today.year}.{today.month}.{today.day}.0"
|
||||
|
||||
# 2. 定义版本信息结构 (PyInstaller 格式)
|
||||
# 语言代码 2052 = 简体中文, 字符集 1200 = Unicode
|
||||
content = f"""# UTF-8
|
||||
VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
filevers={ver_tuple},
|
||||
prodvers={ver_tuple},
|
||||
mask=0x3f,
|
||||
flags=0x0,
|
||||
OS=0x40004,
|
||||
fileType=0x1,
|
||||
subtype=0x0,
|
||||
date=(0, 0)
|
||||
),
|
||||
kids=[
|
||||
StringFileInfo(
|
||||
[
|
||||
StringTable(
|
||||
u'080404b0',
|
||||
[StringStruct(u'CompanyName', u'中能建西北院海上能源业务开发部'),
|
||||
StringStruct(u'FileDescription', u'海上风电场集电线路设计优化系统'),
|
||||
StringStruct(u'FileVersion', u'{ver_str}'),
|
||||
StringStruct(u'InternalName', u'WindFarmOptimizer'),
|
||||
StringStruct(u'LegalCopyright', u'Copyright (c) {today.year}'),
|
||||
StringStruct(u'OriginalFilename', u'海上风电场集电线路设计优化系统.exe'),
|
||||
StringStruct(u'ProductName', u'海上风电场集电线路设计优化系统'),
|
||||
StringStruct(u'ProductVersion', u'{ver_str}')])
|
||||
]),
|
||||
VarFileInfo([VarStruct(u'Translation', [2052, 1200])])
|
||||
]
|
||||
)
|
||||
"""
|
||||
|
||||
with open("version_info.txt", "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
# 3. 同时生成一个 python 文件供 gui.py 调用
|
||||
with open("version.py", "w", encoding="utf-8") as f:
|
||||
f.write(f'VERSION = "v{ver_str}"\n')
|
||||
|
||||
print(f"已生成版本信息文件: version_info.txt 和 version.py (版本: v{ver_str})")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_version_file()
|
||||
472
mip.py
Normal file
472
mip.py
Normal file
@@ -0,0 +1,472 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.spatial import distance_matrix
|
||||
from scipy.sparse.csgraph import minimum_spanning_tree
|
||||
from collections import defaultdict
|
||||
import random
|
||||
|
||||
try:
|
||||
import pulp
|
||||
|
||||
pulp_available = True
|
||||
except ImportError:
|
||||
pulp = None
|
||||
pulp_available = False
|
||||
|
||||
try:
|
||||
import pyomo.environ as pyo_env
|
||||
|
||||
pyomo_available = True
|
||||
except (ImportError, AttributeError):
|
||||
pyomo_available = False
|
||||
print("Pyomo not available, falling back to PuLP")
|
||||
|
||||
|
||||
def design_with_pyomo(
|
||||
turbines,
|
||||
substation,
|
||||
cable_specs=None,
|
||||
voltage=66000,
|
||||
power_factor=0.95,
|
||||
system_params=None,
|
||||
max_clusters=None,
|
||||
time_limit=300,
|
||||
evaluate_func=None,
|
||||
total_invest_func=None,
|
||||
get_max_capacity_func=None,
|
||||
):
|
||||
"""
|
||||
使用Pyomo求解器优化集电线路布局
|
||||
:param turbines: 风机DataFrame
|
||||
:param substation: 升压站坐标
|
||||
:param cable_specs: 电缆规格
|
||||
:param system_params: 系统参数(用于NPV计算)
|
||||
:param max_clusters: 最大簇数,默认基于功率计算
|
||||
:param time_limit: 求解时间限制(秒)
|
||||
:param evaluate_func: 评估函数
|
||||
:param total_invest_func: 总投资计算函数
|
||||
:param get_max_capacity_func: 获取最大容量函数
|
||||
:return: 连接列表和带有簇信息的turbines
|
||||
"""
|
||||
if get_max_capacity_func:
|
||||
max_mw = get_max_capacity_func(cable_specs, voltage, power_factor)
|
||||
else:
|
||||
max_mw = 100.0
|
||||
total_power = turbines["power"].sum()
|
||||
if max_clusters is None:
|
||||
max_clusters = int(np.ceil(total_power / max_mw))
|
||||
n_turbines = len(turbines)
|
||||
|
||||
all_coords = np.vstack([substation, turbines[["x", "y"]].values])
|
||||
dist_matrix_full = distance_matrix(all_coords, all_coords)
|
||||
|
||||
# Simple fallback for now - use PuLP instead
|
||||
print("Pyomo not fully implemented, falling back to PuLP")
|
||||
return design_with_mip(
|
||||
turbines,
|
||||
substation,
|
||||
cable_specs,
|
||||
voltage,
|
||||
power_factor,
|
||||
system_params,
|
||||
max_clusters,
|
||||
time_limit,
|
||||
evaluate_func,
|
||||
total_invest_func,
|
||||
get_max_capacity_func,
|
||||
)
|
||||
|
||||
|
||||
def design_with_mip(
|
||||
turbines,
|
||||
substation,
|
||||
cable_specs=None,
|
||||
voltage=66000,
|
||||
power_factor=0.95,
|
||||
system_params=None,
|
||||
max_clusters=None,
|
||||
time_limit=300,
|
||||
evaluate_func=None,
|
||||
total_invest_func=None,
|
||||
get_max_capacity_func=None,
|
||||
):
|
||||
"""
|
||||
使用混合整数规划(MIP)优化集电线路布局
|
||||
:param turbines: 风机DataFrame
|
||||
:param substation: 升压站坐标
|
||||
:param cable_specs: 电缆规格
|
||||
:param system_params: 系统参数(用于NPV计算)
|
||||
:param max_clusters: 最大簇数,默认基于功率计算
|
||||
:param time_limit: 求解时间限制(秒)
|
||||
:param evaluate_func: 评估函数
|
||||
:param total_invest_func: 总投资计算函数
|
||||
:param get_max_capacity_func: 获取最大容量函数
|
||||
:return: 连接列表和带有簇信息的turbines
|
||||
"""
|
||||
if not pulp_available:
|
||||
print(
|
||||
"WARNING: PuLP library not available. MIP optimization skipped, falling back to MST."
|
||||
)
|
||||
from main import design_with_mst
|
||||
|
||||
connections = design_with_mst(turbines, substation)
|
||||
return connections, turbines
|
||||
|
||||
if get_max_capacity_func:
|
||||
max_mw = get_max_capacity_func(cable_specs, voltage, power_factor)
|
||||
else:
|
||||
max_mw = 100.0
|
||||
if max_clusters is None:
|
||||
max_clusters = int(np.ceil(turbines["power"].sum() / max_mw))
|
||||
n_turbines = len(turbines)
|
||||
|
||||
print(
|
||||
f"MIP Model Setup: n_turbines={n_turbines}, max_clusters={max_clusters}, max_mw={max_mw:.2f} MW"
|
||||
)
|
||||
|
||||
all_coords = np.vstack([substation, turbines[["x", "y"]].values])
|
||||
dist_matrix_full = distance_matrix(all_coords, all_coords)
|
||||
|
||||
prob = pulp.LpProblem("WindFarmCollectorMIP", pulp.LpMinimize)
|
||||
|
||||
# Create all decision variables upfront to avoid duplicates
|
||||
assign_vars = {}
|
||||
for i in range(n_turbines):
|
||||
for k in range(max_clusters):
|
||||
assign_vars[(i, k)] = pulp.LpVariable(f"assign_{i}_{k}", cat="Binary")
|
||||
|
||||
cluster_vars = {}
|
||||
for k in range(max_clusters):
|
||||
cluster_vars[k] = pulp.LpVariable(f"cluster_{k}", cat="Binary")
|
||||
|
||||
# Helper functions to access variables
|
||||
def assign_var(i, k):
|
||||
return assign_vars[(i, k)]
|
||||
|
||||
def cluster_var(k):
|
||||
return cluster_vars[k]
|
||||
|
||||
# Simplified objective function: minimize total distance
|
||||
prob += pulp.lpSum(
|
||||
[
|
||||
dist_matrix_full[0, i + 1] * assign_var(i, k)
|
||||
for i in range(n_turbines)
|
||||
for k in range(max_clusters)
|
||||
]
|
||||
)
|
||||
|
||||
for i in range(n_turbines):
|
||||
prob += pulp.lpSum([assign_var(i, k) for k in range(max_clusters)]) == 1
|
||||
|
||||
for k in range(max_clusters):
|
||||
cluster_power = pulp.lpSum(
|
||||
[turbines.iloc[i]["power"] * assign_var(i, k) for i in range(n_turbines)]
|
||||
)
|
||||
prob += cluster_power <= max_mw * 1.2 * cluster_var(k)
|
||||
|
||||
for k in range(max_clusters):
|
||||
for i in range(n_turbines):
|
||||
prob += assign_var(i, k) <= cluster_var(k)
|
||||
|
||||
print(
|
||||
f"MIP Model: {len(prob.variables())} variables, {len(prob.constraints)} constraints"
|
||||
)
|
||||
|
||||
# Debug: Print model structure
|
||||
print("MIP model structure check:")
|
||||
print(f" Variables: {len(prob.variables())}")
|
||||
print(f" Constraints: {len(prob.constraints)}")
|
||||
print(f" Time limit: {time_limit}s")
|
||||
print(f" Turbines: {n_turbines}, Clusters: {max_clusters}")
|
||||
|
||||
# Test solver availability
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
test_solver = subprocess.run(
|
||||
[
|
||||
r"D:\code\windfarm\.venv\Lib\site-packages\pulp\apis\..\solverdir\cbc\win\i64\cbc.exe",
|
||||
"-version",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
print(
|
||||
f"CBC solver test: {test_solver.stdout[:100] if test_solver.stdout else 'No output'}"
|
||||
)
|
||||
except Exception as solver_test_error:
|
||||
print(f"CBC solver test failed: {solver_test_error}")
|
||||
|
||||
print("MIP: Starting to solve...")
|
||||
try:
|
||||
# Try to use CBC solver with different configurations
|
||||
solver = pulp.PULP_CBC_CMD(
|
||||
timeLimit=time_limit,
|
||||
msg=False,
|
||||
warmStart=False,
|
||||
)
|
||||
print(f"Using CBC solver with time limit: {time_limit}s")
|
||||
status = prob.solve(solver)
|
||||
print(
|
||||
f"MIP: Solver status={pulp.LpStatus[prob.status]}, Objective value={pulp.value(prob.objective):.4f}"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"MIP: CBC solver execution failed: {e}")
|
||||
# Try alternative solver configurations
|
||||
try:
|
||||
print("MIP: Trying alternative solver configuration...")
|
||||
solver = pulp.PULP_CBC_CMD(
|
||||
msg=True, # Enable messages for debugging
|
||||
threads=1, # Single thread
|
||||
timeLimit=time_limit,
|
||||
)
|
||||
status = prob.solve(solver)
|
||||
print(
|
||||
f"MIP: Alternative solver status={pulp.LpStatus[prob.status]}, Objective value={pulp.value(prob.objective):.4f}"
|
||||
)
|
||||
except Exception as e2:
|
||||
print(f"MIP: All solver attempts failed: {e2}, falling back to MST")
|
||||
from main import design_with_mst
|
||||
|
||||
connections = design_with_mst(turbines, substation)
|
||||
return connections, turbines
|
||||
|
||||
if pulp.LpStatus[prob.status] != "Optimal":
|
||||
print(
|
||||
f"MIP solver status: {pulp.LpStatus[prob.status]}, solution not found, falling back to MST"
|
||||
)
|
||||
print("Model feasibility check:")
|
||||
print(f"Total power: {turbines['power'].sum():.2f} MW")
|
||||
print(f"Max cluster capacity: {max_mw:.2f} MW")
|
||||
print(f"Number of clusters: {max_clusters}, Number of turbines: {n_turbines}")
|
||||
|
||||
for k in range(max_clusters):
|
||||
cluster_power = pulp.value(
|
||||
pulp.lpSum(
|
||||
[
|
||||
turbines.iloc[i]["power"] * assign_var(i, k)
|
||||
for i in range(n_turbines)
|
||||
]
|
||||
)
|
||||
)
|
||||
cluster_used = pulp.value(cluster_var(k))
|
||||
print(
|
||||
f"Cluster {k}: Power={cluster_power:.2f} MW (max {max_mw * 1.2:.2f}), Used={cluster_used}"
|
||||
)
|
||||
|
||||
from main import design_with_mst
|
||||
|
||||
connections = design_with_mst(turbines, substation)
|
||||
return connections, turbines
|
||||
|
||||
cluster_assign = [-1] * n_turbines
|
||||
active_clusters = []
|
||||
for k in range(max_clusters):
|
||||
if pulp.value(cluster_var(k)) > 0.5:
|
||||
active_clusters.append(k)
|
||||
|
||||
for i in range(n_turbines):
|
||||
assigned = False
|
||||
for k in active_clusters:
|
||||
if pulp.value(assign_var(i, k)) > 0.5:
|
||||
cluster_assign[i] = k
|
||||
assigned = True
|
||||
break
|
||||
if not assigned:
|
||||
dists = [dist_matrix_full[0, i + 1] for k in active_clusters]
|
||||
cluster_assign[i] = active_clusters[np.argmin(dists)]
|
||||
|
||||
clusters = defaultdict(list)
|
||||
for i, c in enumerate(cluster_assign):
|
||||
clusters[c].append(i)
|
||||
|
||||
connections = []
|
||||
for c, members in clusters.items():
|
||||
if len(members) == 0:
|
||||
continue
|
||||
coords = turbines.iloc[members][["x", "y"]].values
|
||||
if len(members) > 1:
|
||||
dm = distance_matrix(coords, coords)
|
||||
mst = minimum_spanning_tree(dm).toarray()
|
||||
for i in range(len(members)):
|
||||
for j in range(len(members)):
|
||||
if mst[i, j] > 0:
|
||||
connections.append(
|
||||
(
|
||||
f"turbine_{members[i]}",
|
||||
f"turbine_{members[j]}",
|
||||
mst[i, j],
|
||||
)
|
||||
)
|
||||
dists = [dist_matrix_full[0, m + 1] for m in members]
|
||||
closest = members[np.argmin(dists)]
|
||||
connections.append((f"turbine_{closest}", "substation", min(dists)))
|
||||
|
||||
turbines["cluster"] = cluster_assign
|
||||
|
||||
# Check cluster distances
|
||||
min_cluster_distance = check_cluster_distances(clusters, turbines)
|
||||
if min_cluster_distance is not None:
|
||||
print(
|
||||
f"Cluster validation: Minimum distance between clusters = {min_cluster_distance:.2f} m"
|
||||
)
|
||||
if min_cluster_distance < 1000:
|
||||
print(
|
||||
f"WARNING: Clusters are very close to each other ({min_cluster_distance:.2f} m < 1000 m)"
|
||||
)
|
||||
elif min_cluster_distance < 2000:
|
||||
print(
|
||||
f"NOTICE: Clusters are relatively close ({min_cluster_distance:.2f} m)"
|
||||
)
|
||||
|
||||
# Check for cable crossings
|
||||
cable_crossings = check_cable_crossings(connections, turbines, substation)
|
||||
if cable_crossings:
|
||||
print(
|
||||
f"WARNING: Found {len(cable_crossings)} cable crossing(s) in the solution"
|
||||
)
|
||||
for i, (idx1, idx2, p1, p2, p3, p4) in enumerate(cable_crossings):
|
||||
conn1 = connections[idx1]
|
||||
conn2 = connections[idx2]
|
||||
print(
|
||||
f" Crossing {i + 1}: Connection {conn1[0]}-{conn1[1]} crosses {conn2[0]}-{conn2[1]}"
|
||||
)
|
||||
else:
|
||||
print("No cable crossings detected in the solution")
|
||||
|
||||
print(
|
||||
f"MIP optimization completed successfully, {len(connections)} connections generated"
|
||||
)
|
||||
return connections, turbines
|
||||
|
||||
|
||||
def calculate_cluster_centroids(clusters, turbines):
|
||||
"""Calculate the centroid coordinates for each cluster."""
|
||||
centroids = {}
|
||||
for c, members in clusters.items():
|
||||
if len(members) == 0:
|
||||
centroids[c] = (0, 0)
|
||||
else:
|
||||
coords = turbines.iloc[members][["x", "y"]].values
|
||||
centroid_x = np.mean(coords[:, 0])
|
||||
centroid_y = np.mean(coords[:, 1])
|
||||
centroids[c] = (centroid_x, centroid_y)
|
||||
return centroids
|
||||
|
||||
|
||||
def check_cluster_distances(clusters, turbines, min_distance_threshold=1000):
|
||||
"""Check if any clusters are too close to each other."""
|
||||
if len(clusters) < 2:
|
||||
return None
|
||||
|
||||
centroids = calculate_cluster_centroids(clusters, turbines)
|
||||
active_clusters = [c for c, members in clusters.items() if len(members) > 0]
|
||||
|
||||
min_distance = float("inf")
|
||||
min_pair = None
|
||||
|
||||
for i in range(len(active_clusters)):
|
||||
for j in range(i + 1, len(active_clusters)):
|
||||
c1, c2 = active_clusters[i], active_clusters[j]
|
||||
centroid1 = np.array(centroids[c1])
|
||||
centroid2 = np.array(centroids[c2])
|
||||
distance = np.linalg.norm(centroid1 - centroid2)
|
||||
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
min_pair = (c1, c2)
|
||||
|
||||
return min_distance
|
||||
|
||||
|
||||
def check_cable_crossings(connections, turbines, substation):
|
||||
"""Check if there are cable crossings in the solution."""
|
||||
crossings = []
|
||||
|
||||
def line_intersection(p1, p2, p3, p4):
|
||||
"""Check if line segments (p1,p2) and (p3,p4) intersect."""
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
x3, y3 = p3
|
||||
x4, y4 = p4
|
||||
|
||||
denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
||||
|
||||
if abs(denom) < 1e-10:
|
||||
return False
|
||||
|
||||
ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom
|
||||
ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom
|
||||
|
||||
return 0 <= ua <= 1 and 0 <= ub <= 1
|
||||
|
||||
def get_turbine_coord(connection_part):
|
||||
"""Get coordinates from connection part (turbine_# or substation)."""
|
||||
if connection_part == "substation":
|
||||
# Handle different substation formats robustly
|
||||
if isinstance(substation, np.ndarray):
|
||||
if substation.ndim == 1:
|
||||
# 1D array [x, y]
|
||||
return (substation[0], substation[1])
|
||||
elif substation.ndim == 2:
|
||||
# 2D array [[x, y]] or shape (n, 2)
|
||||
if substation.shape[0] == 1:
|
||||
return (substation[0, 0], substation[0, 1])
|
||||
else:
|
||||
# Multiple points, use first one
|
||||
return (substation[0, 0], substation[0, 1])
|
||||
else:
|
||||
# Unexpected dimension, try fallback
|
||||
return (substation.flat[0], substation.flat[1])
|
||||
elif isinstance(substation, (list, tuple)):
|
||||
# List or tuple format
|
||||
# Handle nested lists like [[x, y]]
|
||||
if (
|
||||
isinstance(substation[0], (list, tuple, np.ndarray))
|
||||
and len(substation[0]) >= 2
|
||||
):
|
||||
return (substation[0][0], substation[0][1])
|
||||
elif len(substation) >= 2:
|
||||
return (substation[0], substation[1])
|
||||
else:
|
||||
return (float("inf"), float("inf"))
|
||||
else:
|
||||
# Unexpected format, try to convert
|
||||
try:
|
||||
sub_array = np.array(substation)
|
||||
if sub_array.ndim == 1:
|
||||
return (sub_array[0], sub_array[1])
|
||||
else:
|
||||
return (sub_array.flat[0], sub_array.flat[1])
|
||||
except:
|
||||
return (float("inf"), float("inf"))
|
||||
else:
|
||||
turbine_idx = int(connection_part.split("_")[1])
|
||||
return (
|
||||
turbines.iloc[turbine_idx]["x"],
|
||||
turbines.iloc[turbine_idx]["y"],
|
||||
)
|
||||
|
||||
for i in range(len(connections)):
|
||||
for j in range(i + 1, len(connections)):
|
||||
conn1 = connections[i]
|
||||
conn2 = connections[j]
|
||||
|
||||
p1 = get_turbine_coord(conn1[0])
|
||||
p2 = get_turbine_coord(conn1[1])
|
||||
p3 = get_turbine_coord(conn2[0])
|
||||
p4 = get_turbine_coord(conn2[1])
|
||||
|
||||
if (
|
||||
np.array_equal(p1, p3)
|
||||
or np.array_equal(p1, p4)
|
||||
or np.array_equal(p2, p3)
|
||||
or np.array_equal(p2, p4)
|
||||
):
|
||||
continue
|
||||
|
||||
if line_intersection(p1, p2, p3, p4):
|
||||
crossings.append((i, j, p1, p2, p3, p4))
|
||||
|
||||
return crossings
|
||||
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.
|
||||
25
pyproject.toml
Normal file
25
pyproject.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[project]
|
||||
name = "windfarm"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"ezdxf>=1.4.3",
|
||||
"matplotlib>=3.10.8",
|
||||
"networkx>=3.6.1",
|
||||
"nicegui>=3.4.1",
|
||||
"numpy>=2.4.0",
|
||||
"openpyxl>=3.1.5",
|
||||
"pandas>=2.3.3",
|
||||
"pulp>=3.3.0",
|
||||
"pyomo>=6.9.5",
|
||||
"pywebview>=6.1",
|
||||
"scikit-learn>=1.8.0",
|
||||
"scipy>=1.16.3",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pyinstaller>=6.17.0",
|
||||
]
|
||||
146
test_cbc_solver.py
Normal file
146
test_cbc_solver.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Simple test to verify CBC solver functionality
|
||||
"""
|
||||
|
||||
import pulp
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
print("=== PuLP and CBC Solver Test ===")
|
||||
print(f"Python version: {sys.version}")
|
||||
print(f"PuLP version: {pulp.__version__}")
|
||||
|
||||
# Test 1: Check PuLP installation
|
||||
print("\n1. Checking PuLP installation...")
|
||||
try:
|
||||
from pulp import LpProblem, LpVariable, LpMinimize, LpMaximize, lpSum, value
|
||||
|
||||
print("[OK] PuLP imported successfully")
|
||||
except ImportError as e:
|
||||
print(f"[FAIL] PuLP import failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 2: Check CBC solver file existence
|
||||
print("\n2. Checking CBC solver file...")
|
||||
solver_dir = os.path.join(
|
||||
os.path.dirname(pulp.__file__), "apis", "..", "solverdir", "cbc", "win", "i64"
|
||||
)
|
||||
solver_path = os.path.join(solver_dir, "cbc.exe")
|
||||
|
||||
print(f"Looking for CBC at: {solver_path}")
|
||||
if os.path.exists(solver_path):
|
||||
print(f"[OK] CBC solver file found")
|
||||
file_size = os.path.getsize(solver_path)
|
||||
print(f" File size: {file_size:,} bytes ({file_size / 1024 / 1024:.2f} MB)")
|
||||
else:
|
||||
print(f"[FAIL] CBC solver file not found")
|
||||
print(f" Checking directory contents:")
|
||||
try:
|
||||
parent_dir = os.path.dirname(solver_path)
|
||||
if os.path.exists(parent_dir):
|
||||
for item in os.listdir(parent_dir):
|
||||
print(f" - {item}")
|
||||
else:
|
||||
print(f" Directory does not exist: {parent_dir}")
|
||||
except Exception as e:
|
||||
print(f" Error listing directory: {e}")
|
||||
|
||||
# Test 3: Try to run CBC solver directly
|
||||
print("\n3. Testing CBC solver execution...")
|
||||
if os.path.exists(solver_path):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[solver_path, "-version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
check=True,
|
||||
)
|
||||
print("[OK] CBC solver executed successfully")
|
||||
print(f" Output: {result.stdout[:200]}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"[FAIL] CBC solver execution failed (exit code {e.returncode})")
|
||||
print(f" stdout: {e.stdout[:200]}")
|
||||
print(f" stderr: {e.stderr[:200]}")
|
||||
except subprocess.TimeoutExpired:
|
||||
print("[FAIL] CBC solver execution timed out")
|
||||
except Exception as e:
|
||||
print(f"[FAIL] CBC solver execution error: {e}")
|
||||
else:
|
||||
print("[FAIL] Cannot test CBC execution - file not found")
|
||||
|
||||
# Test 4: Solve a simple linear programming problem
|
||||
print("\n4. Testing simple LP problem...")
|
||||
try:
|
||||
# Simple problem: minimize x + y subject to x + y >= 5, x >= 0, y >= 0
|
||||
prob = LpProblem("Simple_LP_Test", LpMinimize)
|
||||
|
||||
x = LpVariable("x", lowBound=0, cat="Continuous")
|
||||
y = LpVariable("y", lowBound=0, cat="Continuous")
|
||||
|
||||
prob += x + y # Objective: minimize x + y
|
||||
prob += x + y >= 5 # Constraint
|
||||
|
||||
print(" Created simple LP problem: minimize x + y subject to x + y >= 5")
|
||||
|
||||
# Try to solve with CBC
|
||||
solver = pulp.PULP_CBC_CMD(msg=False, timeLimit=10)
|
||||
print(" Attempting to solve with CBC...")
|
||||
|
||||
status = prob.solve(solver)
|
||||
|
||||
print(f"[OK] LP problem solved")
|
||||
print(f" Status: {pulp.LpStatus[prob.status]}")
|
||||
print(f" Objective value: {value(prob.objective)}")
|
||||
print(f" x = {value(x)}, y = {value(y)}")
|
||||
|
||||
if abs(value(prob.objective) - 5.0) < 0.01:
|
||||
print(" [OK] Correct solution found!")
|
||||
else:
|
||||
print(f" [FAIL] Unexpected solution (expected 5.0)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[FAIL] LP problem solving failed: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
# Test 5: Solve a simple mixed integer programming problem
|
||||
print("\n5. Testing simple MIP problem...")
|
||||
try:
|
||||
# Simple MIP: minimize x + y subject to x + y >= 5, x, y integers >= 0
|
||||
prob = LpProblem("Simple_MIP_Test", LpMinimize)
|
||||
|
||||
x = LpVariable("x", lowBound=0, cat="Integer")
|
||||
y = LpVariable("y", lowBound=0, cat="Integer")
|
||||
|
||||
prob += x + y # Objective
|
||||
prob += x + y >= 5 # Constraint
|
||||
|
||||
print(
|
||||
" Created simple MIP problem: minimize x + y subject to x + y >= 5, x,y integers"
|
||||
)
|
||||
|
||||
solver = pulp.PULP_CBC_CMD(msg=False, timeLimit=10)
|
||||
print(" Attempting to solve with CBC...")
|
||||
|
||||
status = prob.solve(solver)
|
||||
|
||||
print(f"[OK] MIP problem solved")
|
||||
print(f" Status: {pulp.LpStatus[prob.status]}")
|
||||
print(f" Objective value: {value(prob.objective)}")
|
||||
print(f" x = {value(x)}, y = {value(y)}")
|
||||
|
||||
if abs(value(prob.objective) - 5.0) < 0.01:
|
||||
print(" [OK] Correct solution found!")
|
||||
else:
|
||||
print(f" [FAIL] Unexpected solution (expected 5.0)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[FAIL] MIP problem solving failed: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
print("\n=== Test Complete ===")
|
||||
50
test_mip.py
Normal file
50
test_mip.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Test script to verify MIP functionality
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from mip import design_with_mip
|
||||
|
||||
# Create test data
|
||||
np.random.seed(42)
|
||||
n_turbines = 10
|
||||
turbines = pd.DataFrame(
|
||||
{
|
||||
"x": np.random.uniform(0, 2000, n_turbines),
|
||||
"y": np.random.uniform(0, 2000, n_turbines),
|
||||
"power": np.random.uniform(5, 10, n_turbines),
|
||||
}
|
||||
)
|
||||
|
||||
substation = np.array([1000, 1000])
|
||||
|
||||
print("Test data created:")
|
||||
print(f"Number of turbines: {n_turbines}")
|
||||
print(f"Substation location: {substation}")
|
||||
print(f"Total power: {turbines['power'].sum():.2f} MW")
|
||||
|
||||
# Test MIP function
|
||||
print("\nTesting MIP design...")
|
||||
try:
|
||||
connections, turbines_with_clusters = design_with_mip(
|
||||
turbines,
|
||||
substation,
|
||||
cable_specs=None,
|
||||
voltage=66000,
|
||||
power_factor=0.95,
|
||||
system_params=None,
|
||||
max_clusters=None,
|
||||
time_limit=30,
|
||||
evaluate_func=None,
|
||||
total_invest_func=None,
|
||||
get_max_capacity_func=None,
|
||||
)
|
||||
print(f"MIP test successful!")
|
||||
print(f"Number of connections: {len(connections)}")
|
||||
print(f"Clusters assigned: {turbines_with_clusters['cluster'].tolist()}")
|
||||
except Exception as e:
|
||||
print(f"MIP test failed with error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
232
win32_helper.py
Normal file
232
win32_helper.py
Normal file
@@ -0,0 +1,232 @@
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
import os
|
||||
|
||||
def show_save_dialog_win32():
|
||||
"""
|
||||
使用 ctypes 直接调用 Windows API (GetSaveFileNameW)
|
||||
不需要子进程,可以在线程中运行 (run.io_bound)
|
||||
"""
|
||||
try:
|
||||
# 定义 OPENFILENAME 结构体
|
||||
class OPENFILENAME(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("lStructSize", ctypes.wintypes.DWORD),
|
||||
("hwndOwner", ctypes.wintypes.HWND),
|
||||
("hInstance", ctypes.wintypes.HINSTANCE),
|
||||
("lpstrFilter", ctypes.wintypes.LPCWSTR),
|
||||
("lpstrCustomFilter", ctypes.wintypes.LPWSTR),
|
||||
("nMaxCustFilter", ctypes.wintypes.DWORD),
|
||||
("nFilterIndex", ctypes.wintypes.DWORD),
|
||||
("lpstrFile", ctypes.wintypes.LPWSTR),
|
||||
("nMaxFile", ctypes.wintypes.DWORD),
|
||||
("lpstrFileTitle", ctypes.wintypes.LPWSTR),
|
||||
("nMaxFileTitle", ctypes.wintypes.DWORD),
|
||||
("lpstrInitialDir", ctypes.wintypes.LPCWSTR),
|
||||
("lpstrTitle", ctypes.wintypes.LPCWSTR),
|
||||
("Flags", ctypes.wintypes.DWORD),
|
||||
("nFileOffset", ctypes.wintypes.WORD),
|
||||
("nFileExtension", ctypes.wintypes.WORD),
|
||||
("lpstrDefExt", ctypes.wintypes.LPCWSTR),
|
||||
("lCustData", ctypes.wintypes.LPARAM),
|
||||
("lpfnHook", ctypes.wintypes.LPVOID),
|
||||
("lpTemplateName", ctypes.wintypes.LPCWSTR),
|
||||
# 还有更多字段,但这通常足够了
|
||||
# ("pvReserved", ctypes.wintypes.LPVOID),
|
||||
# ("dwReserved", ctypes.wintypes.DWORD),
|
||||
# ("FlagsEx", ctypes.wintypes.DWORD),
|
||||
]
|
||||
|
||||
# 准备缓冲区
|
||||
filename_buffer = ctypes.create_unicode_buffer(260) # MAX_PATH
|
||||
# 设置初始文件名
|
||||
filename_buffer.value = "win32_save.xlsx"
|
||||
|
||||
# 准备过滤器 (用 \0 分隔)
|
||||
# 格式: "描述\0模式\0描述\0模式\0\0"
|
||||
filter_str = "Excel Files (*.xlsx)\0*.xlsx\0All Files (*.*)\0*.*\0\0"
|
||||
|
||||
ofn = OPENFILENAME()
|
||||
ofn.lStructSize = ctypes.sizeof(OPENFILENAME)
|
||||
ofn.hwndOwner = 0 # NULL
|
||||
ofn.lpstrFilter = filter_str
|
||||
ofn.lpstrFile = ctypes.cast(filename_buffer, ctypes.wintypes.LPWSTR)
|
||||
ofn.nMaxFile = 260
|
||||
ofn.lpstrDefExt = "xlsx"
|
||||
ofn.lpstrTitle = "保存文件 (Win32 API)"
|
||||
# OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR
|
||||
ofn.Flags = 0x00000002 | 0x00000800 | 0x00000008
|
||||
|
||||
comdlg32 = ctypes.windll.comdlg32
|
||||
|
||||
# 调用 API
|
||||
# GetSaveFileNameW 返回非零值表示成功
|
||||
if comdlg32.GetSaveFileNameW(ctypes.byref(ofn)):
|
||||
return filename_buffer.value
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Win32 API Error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def show_save_dialog_com():
|
||||
"""
|
||||
使用 COM 接口 IFileSaveDialog (Windows Vista+)
|
||||
提供更现代化的文件保存对话框,支持更多功能
|
||||
"""
|
||||
try:
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
import uuid
|
||||
|
||||
# 定义必要的常量
|
||||
CLSCTX_INPROC_SERVER = 1
|
||||
S_OK = 0
|
||||
FOS_OVERWRITEPROMPT = 0x00000002
|
||||
FOS_PATHMUSTEXIST = 0x00000800
|
||||
FOS_NOCHANGEDIR = 0x00000008
|
||||
SIGDN_FILESYSPATH = 0x80058000
|
||||
|
||||
# IFileSaveDialog 的 CLSID 和 IID
|
||||
CLSID_FileSaveDialog = uuid.UUID("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}")
|
||||
IID_IFileSaveDialog = uuid.UUID("{84bccd23-5fde-4cdb-aea4-af64b83d78ab}")
|
||||
IID_IShellItem = uuid.UUID("{43826d1e-e718-42ee-bc55-a1e261c37bfe}")
|
||||
|
||||
# 加载 ole32.dll
|
||||
ole32 = ctypes.windll.ole32
|
||||
|
||||
# CoInitialize
|
||||
ole32.CoInitialize(None)
|
||||
|
||||
# CoCreateInstance
|
||||
p_dialog = ctypes.c_void_p()
|
||||
hr = ole32.CoCreateInstance(
|
||||
ctypes.byref(CLSID_FileSaveDialog),
|
||||
None,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
ctypes.byref(IID_IFileSaveDialog),
|
||||
ctypes.byref(p_dialog)
|
||||
)
|
||||
|
||||
if hr != S_OK:
|
||||
print(f"CoCreateInstance failed: {hr}")
|
||||
return None
|
||||
|
||||
# 定义 IFileSaveDialog 的 vtable 方法
|
||||
# 我们只需要调用 Show, GetResult, SetOptions, SetFileName, SetDefaultExtension, SetFileTypeIndex
|
||||
# 这些方法在 IFileOpenDialog 基类中定义
|
||||
|
||||
# SetOptions
|
||||
class IFileSaveDialogVtbl(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("QueryInterface", ctypes.c_void_p),
|
||||
("AddRef", ctypes.c_void_p),
|
||||
("Release", ctypes.c_void_p),
|
||||
# IModalWindow
|
||||
("Show", ctypes.c_void_p),
|
||||
# IFileDialog
|
||||
("SetFileTypes", ctypes.c_void_p),
|
||||
("SetFileTypeIndex", ctypes.c_void_p),
|
||||
("GetFileTypeIndex", ctypes.c_void_p),
|
||||
("Advise", ctypes.c_void_p),
|
||||
("Unadvise", ctypes.c_void_p),
|
||||
("SetOptions", ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_ulong)),
|
||||
("GetOptions", ctypes.c_void_p),
|
||||
("SetDefaultFolder", ctypes.c_void_p),
|
||||
("SetFolder", ctypes.c_void_p),
|
||||
("GetFolder", ctypes.c_void_p),
|
||||
("GetCurrentSelection", ctypes.c_void_p),
|
||||
("SetFileName", ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_wchar_p)),
|
||||
("GetFileName", ctypes.c_void_p),
|
||||
("SetTitle", ctypes.c_void_p),
|
||||
("SetOkButtonLabel", ctypes.c_void_p),
|
||||
("SetFileNameLabel", ctypes.c_void_p),
|
||||
("GetResult", ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p))),
|
||||
("AddPlace", ctypes.c_void_p),
|
||||
("SetDefaultExtension", ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_wchar_p)),
|
||||
("Close", ctypes.c_void_p),
|
||||
("SetClientGuid", ctypes.c_void_p),
|
||||
("ClearClientData", ctypes.c_void_p),
|
||||
("SetFilter", ctypes.c_void_p),
|
||||
]
|
||||
|
||||
# 获取 vtable
|
||||
vtable = ctypes.cast(p_dialog, ctypes.POINTER(ctypes.POINTER(IFileSaveDialogVtbl))).contents.contents
|
||||
|
||||
# 调用 SetOptions
|
||||
hr = vtable.SetOptions(p_dialog, FOS_OVERWRITEPROMPT | FOS_PATHMUSTEXIST | FOS_NOCHANGEDIR)
|
||||
if hr != S_OK:
|
||||
print(f"SetOptions failed: {hr}")
|
||||
return None
|
||||
|
||||
# 调用 SetFileName
|
||||
hr = vtable.SetFileName(p_dialog, "com_save.xlsx")
|
||||
if hr != S_OK:
|
||||
print(f"SetFileName failed: {hr}")
|
||||
return None
|
||||
|
||||
# 调用 SetDefaultExtension
|
||||
hr = vtable.SetDefaultExtension(p_dialog, "xlsx")
|
||||
if hr != S_OK:
|
||||
print(f"SetDefaultExtension failed: {hr}")
|
||||
return None
|
||||
|
||||
# 调用 SetFileTypeIndex
|
||||
hr = vtable.SetFileTypeIndex(p_dialog, 1)
|
||||
if hr != S_OK:
|
||||
print(f"SetFileTypeIndex failed: {hr}")
|
||||
return None
|
||||
|
||||
# 调用 Show
|
||||
hr = vtable.Show(p_dialog, 0) # 0 表示没有父窗口
|
||||
if hr != S_OK:
|
||||
# 用户取消
|
||||
return None
|
||||
|
||||
# 调用 GetResult
|
||||
p_result = ctypes.c_void_p()
|
||||
hr = vtable.GetResult(p_dialog, ctypes.byref(p_result))
|
||||
if hr != S_OK:
|
||||
print(f"GetResult failed: {hr}")
|
||||
return None
|
||||
|
||||
# 定义 IShellItem 接口
|
||||
class IShellItemVtbl(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("QueryInterface", ctypes.c_void_p),
|
||||
("AddRef", ctypes.c_void_p),
|
||||
("Release", ctypes.c_void_p),
|
||||
("BindToHandler", ctypes.c_void_p),
|
||||
("GetParent", ctypes.c_void_p),
|
||||
("GetDisplayName", ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(ctypes.c_wchar_p))),
|
||||
("GetAttributes", ctypes.c_void_p),
|
||||
("Compare", ctypes.c_void_p),
|
||||
]
|
||||
|
||||
# 获取 IShellItem 的 vtable
|
||||
result_vtable = ctypes.cast(p_result, ctypes.POINTER(ctypes.POINTER(IShellItemVtbl))).contents.contents
|
||||
|
||||
# 调用 GetDisplayName
|
||||
p_display_name = ctypes.c_wchar_p()
|
||||
hr = result_vtable.GetDisplayName(p_result, SIGDN_FILESYSPATH, ctypes.byref(p_display_name))
|
||||
if hr != S_OK:
|
||||
print(f"GetDisplayName failed: {hr}")
|
||||
return None
|
||||
|
||||
filepath = p_display_name.value
|
||||
|
||||
# 清理
|
||||
ole32.CoUninitialize()
|
||||
|
||||
return filepath
|
||||
|
||||
except ImportError as e:
|
||||
print(f"COM Error: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"COM Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
BIN
使用说明/CAD图纸效果示例.png
Normal file
BIN
使用说明/CAD图纸效果示例.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
0
使用说明/pandoc
Normal file
0
使用说明/pandoc
Normal file
165
使用说明/使用说明.md
Normal file
165
使用说明/使用说明.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# 海上风电场集电线路设计优化软件 - 操作手册
|
||||
|
||||
**文档版本:** v1.0
|
||||
**适用对象:** 海上能源业务开发部 - 电气专业
|
||||
**编制日期:** 2026年1月5日
|
||||
|
||||
---
|
||||
|
||||
## 1. 软件概述
|
||||
|
||||
海上风电集电线路作为风电机组与海上升压站的关键连接纽带,其优化设计对提升风电场全生命周期经济性及降低环境影响具有核心意义,不仅能通过拓扑与路由优化减少建设运维成本、降低线路损耗,还能为深远海、大容量风电场开发提供技术支撑。该优化工作面临拓扑与载流量多约束耦合的技术瓶颈,需综合运用数字化技术、先进优化算法进行应对。
|
||||
|
||||
本软件专为海上风电场内集电系统(35kV/66kV/110kV)设计,旨在通过多种先进的拓扑优化算法(如Esau-Williams、MST、旋转扫描法),辅助电气专业快速完成集电线路的路径规划与经济性比选。
|
||||
|
||||
软件能够根据风机坐标、海缆载流量及造价数据,自动计算并生成线损最小、投资最优的接线方案,并支持一键导出 CAD 图纸和海缆长度。
|
||||
|
||||
---
|
||||
|
||||
## 2. 准备工作:输入数据编制
|
||||
|
||||
软件通过读取 Excel 文件获取设计输入条件。为了确保计算准确,请严格按照以下格式准备数据。
|
||||
|
||||
### 2.1 获取标准模板
|
||||
启动软件后,点击左侧配置面板顶部的 **“导出 Excel 模板”** 按钮,保存 `coordinates.xlsx` 文件。建议在此模板基础上进行修改。
|
||||
|
||||

|
||||
|
||||
### 2.2 数据表详解
|
||||
|
||||
输入文件通常包含三个 Sheet(工作表):
|
||||

|
||||
#### (1) Coordinates (坐标数据) - **[必须]**
|
||||
录入升压站和所有风机的位置及参数。
|
||||
* **Type**: 填写 `Substation` (升压站) 或 `Turbine` (风机)。
|
||||
* **ID**: 设备编号(如 Sub1, 01, 02...)。
|
||||
* **X / Y**: 投影坐标(单位:米)。建议使用高斯投影坐标 (X, Y),以保证距离计算准确。
|
||||
* **Power**: 设备功率(单位:MW)。升压站填 0。
|
||||
* **PlatformHeight**: 塔筒/升压站平台高度(单位:米),用于计算海缆爬升段长度。
|
||||
|
||||
<!--  -->
|
||||
|
||||
#### (2) Cables (电缆规格) - **[必须]**
|
||||
录入本项目拟采用的海缆截面库。
|
||||
* **Section**: 导体截面 (mm²)。
|
||||
* **Capacity**: 额定载流量 (A)。**注意:** 需填入考虑降容系数后的实际允许载流量。
|
||||
* **Resistance**: 交流电阻 (Ω/km)。
|
||||
* **Cost**: 综合单价 (元/m)。包含本体及敷设费用。
|
||||
* **Optional**: 可选标记。
|
||||
* 填 `Y`: 表示该型号为“可选大截面电缆”(例如仅在特定大负荷回路使用)。
|
||||
* *规则:* 列表中必须按截面**从小到大**排序。
|
||||
|
||||
#### (3) Parameters (系统参数) - **[必须]**
|
||||
定义系统级电气参数。
|
||||
* **Voltage**: 集电系统标称电压(如 66 或 35),单位 kV。
|
||||
* **Power Factor**: 系统功率因数(如 0.95)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 算法与优化策略介绍
|
||||
|
||||
本软件内置了四种不同机制的拓扑优化算法,分别适用于不同的工程场景。在点击“运行”后,系统会并发执行所有算法,模型自动筛选最优解。
|
||||
|
||||
### 3.1 最小生成树算法 (MST Method)
|
||||
* **原理**:基于图论中的 Kruskal 或 Prim 算法,寻找连接所有风机且总路径长度最短的树状结构。此算法**不考虑电缆载流量限制**。
|
||||
* **作用**:仅作为理论上的“距离基准”参考。它展示了如果导线无限粗、电流无限制情况下的最短布线可能。在实际工程中通常不可行,但可用于评估其他方案的距离效率。
|
||||
|
||||
### 3.2 基础扇区扫描法 (Capacitated Sweep - Base)
|
||||
* **原理**:以升压站为中心,将平面划分为若干个扇区。算法按顺时针方向扫描风机,一旦累积功率达到当前最大电缆的承载上限,就进行“切分”,形成一个独立的集电回路。
|
||||
* **特点**:计算速度极快,拓扑结构简单清晰,类似于人工排布的辐射状接线。
|
||||
* **局限**:对起始扫描角度敏感,可能因为恰好在某个位置切分而导致该回路包含距离很远的风机。
|
||||
|
||||
### 3.3 旋转扫描优化法 (Rotational Sweep)
|
||||
* **原理**:这是对“基础扇区扫描法”的增强版。它会自动尝试 0° 到 360° 之间的所有可能的起始扫描角度。
|
||||
* **优势**:通过旋转扫描角度,可以有效避免因特定方位角切分不当造成的“长尾巴”连线,通常能比基础扫描法节省 3%~8% 的线缆成本。这是最接近人工精细化排布的自动化算法。
|
||||
|
||||
### 3.4 Esau-Williams 启发式算法 (Esau-Williams Heuristic)
|
||||
* **原理**:经典的约束最小生成树(CMST)算法。它从“所有风机都直连升压站”的初始状态开始,迭代计算“将两台风机互联并断开其中一条回升压站连线”所能带来的成本节省(Trade-off)。在满足载流量约束的前提下,优先执行节省最大的互联操作。
|
||||
* **优势**:能够跳出辐射状的思维定式,自动发现树状、多分叉等复杂但更经济的拓扑结构。在风机分布不规则、离岸距离较远或电缆造价极高的情况下,往往能得到比扫描法更优的结果。
|
||||
|
||||
---
|
||||
|
||||
## 4. 操作流程
|
||||
|
||||
### 步骤一:启动软件
|
||||
双击运行程序,等待主界面加载完成。界面分为上侧“操作区”和下侧“结果展示区”。
|
||||
|
||||
### 步骤二:上传数据
|
||||
在上侧“配置面板”中,点击 **“选择Excel文件”** 区域(或点击云朵图标),选择编制好的项目 Excel 文件。
|
||||
|
||||
上传成功后:
|
||||
1. 文件名右侧会出现绿色对勾。
|
||||
2. 右侧信息面板会自动解析并显示**系统参数**(电压、功率因数)和**电缆规格列表**,请务必核对这些数据是否正确。
|
||||
|
||||

|
||||
|
||||
### 步骤三:运行计算
|
||||
点击左侧下方的大型按钮 **“运行方案对比”**。
|
||||
|
||||
软件将自动执行以下计算任务:
|
||||
1. **多场景分析**:
|
||||
* *Scenario 1 (标准)*:仅使用标准电缆库进行优化。
|
||||
* *Scenario 2 (含可选)*:尝试引入更大截面的可选电缆,评估是否能减少回路数。
|
||||
* *Scenario 3 (限制)*:模拟最大截面电缆缺货情况下的次优方案。
|
||||
2. **多算法寻优**:对每个场景同时运行 MST、基础扫描、旋转扫描、Esau-Williams 等多种算法。
|
||||
|
||||
*注意:计算过程中下方黑色日志窗口会实时滚动显示计算进度,通常耗时 10-60 秒,取决于风机数量。*
|
||||
|
||||

|
||||
|
||||
### 步骤四:查看与比选
|
||||
计算完成后,系统会自动筛选出**综合造价最低**的推荐方案,并在界面上展示。
|
||||
|
||||
1. **结果列表**:
|
||||
右侧中部的表格列出了所有计算出的可行方案。
|
||||
* `Cost (万元)`:总投资估算。
|
||||
* `Loss (kW)`:全场集电线路总线损。
|
||||
* **操作**:点击表格中的任意一行,下方的拓扑图会自动切换到该方案。
|
||||
|
||||
2. **拓扑可视化**:
|
||||
右侧下方的绘图区展示集电线路走向。
|
||||
* 不同颜色的线条代表不同截面的海缆。
|
||||
* 图例会标明线型对应的截面。
|
||||
* 升压站显示为红色方块,风机为圆点。
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 5. 成果导出
|
||||
|
||||
比选确定最终方案后,可以使用底部的 **“导出与下载”** 功能区生成设计文件。
|
||||
|
||||
### 5.1 导出 CAD 图纸 (.dxf)
|
||||
* **导出推荐方案**:直接点击 **“导出推荐方案 DXF”**。
|
||||
* **导出特定方案**:在表格中选中任意一行,点击 **“导出选中方案 DXF”**。
|
||||
|
||||
生成的 DXF 文件特点:
|
||||
* **分层管理**:不同截面的电缆位于不同图层(Layer),方便在 AutoCAD 中通过图层过滤器批量修改线型或颜色。
|
||||
* **地理坐标**:图纸保留了 Excel 中的原始坐标系,可直接通过“原点粘贴”功能合并到项目总图中。
|
||||
|
||||

|
||||
|
||||
### 5.2 导出 Excel 报告
|
||||
点击 **“下载 Excel 对比表”**,将生成一份包含详细工程数据的 Excel 文件,内容包括:
|
||||
* **Summary**: 所有方案的经济技术指标汇总。
|
||||
* **Details**: 推荐方案的每一条海缆连接明细(起点、终点、长度、型号)。
|
||||
|
||||
### 5.3 批量归档
|
||||
点击 **“导出全部方案 DXF (ZIP)”**,可将所有计算产生的方案图纸和报表打包下载,便于项目归档。
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题 (FAQ)
|
||||
|
||||
**Q: 为什么上传文件后提示“电缆数据校验失败”?**
|
||||
A: 请检查 `Cables` 表。电缆必须严格按照**截面从小到大**排列,且载流量也必须随截面增加而增加。如果定义了 `Optional` 电缆,它必须是列表中截面最大的一条。
|
||||
|
||||
**Q: 计算出的方案有的回路负载率过高怎么办?**
|
||||
A: 软件算法以不超过额定载流量为约束条件(默认允许 100% 满载)。在实际工程中,建议在 `Cables` 表录入载流量时,预先乘以 0.95 或其他安全系数,留出裕度。
|
||||
|
||||
**Q: 图纸导出后,在 CAD 里看不到东西?**
|
||||
A: 请双击鼠标滚轮(Zoom Extents)全屏显示。由于风机坐标通常是大地坐标(数值很大),如果 CAD 当前视口在 (0,0) 附近,可能会找不到图形。
|
||||
|
||||
---
|
||||
**技术支持:** 海上能源业务开发部 - 杜孟远
|
||||
BIN
使用说明/使用说明.pdf
Normal file
BIN
使用说明/使用说明.pdf
Normal file
Binary file not shown.
BIN
使用说明/导出模板按钮截图.png
Normal file
BIN
使用说明/导出模板按钮截图.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
BIN
使用说明/数据加载成功截图.png
Normal file
BIN
使用说明/数据加载成功截图.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
BIN
使用说明/结果比选交互截图.png
Normal file
BIN
使用说明/结果比选交互截图.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
BIN
使用说明/计算过程截图.png
Normal file
BIN
使用说明/计算过程截图.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
BIN
使用说明/输入数据表.png
Normal file
BIN
使用说明/输入数据表.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
38
海上风电场集电线路设计优化系统.spec
Normal file
38
海上风电场集电线路设计优化系统.spec
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['gui.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('D:\\code\\windfarm\\.venv\\Lib\\site-packages\\nicegui', 'nicegui'), ('version.py', '.')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='海上风电场集电线路设计优化系统',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
Reference in New Issue
Block a user