Compare commits

..

44 Commits

Author SHA1 Message Date
dmy
2b3ab83d91 Fix substation coordinate handling for cable crossing detection 2026-01-08 16:05:06 +08:00
dmy
68df76702e Fix cable crossing detection coordinate unpacking error 2026-01-08 15:53:15 +08:00
dmy
b3a4513f94 Fix MIP solver variable duplication and function structure 2026-01-08 15:30:36 +08:00
dmy
04a5e19451 Improve MIP optimization and add log export feature 2026-01-08 15:08:04 +08:00
dmy
ebd5883dbf Fix unreachable code in design_with_pyomo function 2026-01-08 13:06:26 +08:00
dmy
41ac6f3963 Change MIP objective function to minimize total investment 2026-01-08 13:01:36 +08:00
dmy
09b2ada5df Add debug prints to check MIP toggle functionality 2026-01-08 12:58:15 +08:00
dmy
6441ddc059 Fix MIP import issue: move design_with_mst function outside __main__ protection block 2026-01-08 12:39:07 +08:00
dmy
2f095df12e Fix MIP algorithm: simplify model formulation and add detailed debugging 2026-01-08 10:28:35 +08:00
dmy
a3837a6707 Rewrite MIP model formulation and add comprehensive debugging 2026-01-08 10:22:39 +08:00
dmy
886fba4d15 Clear comparison results and topology visualization on new file upload 2026-01-08 10:10:46 +08:00
dmy
397ca8847e Fix MIP fallback return values: ensure consistent unpacking 2026-01-08 10:06:46 +08:00
dmy
6ad11a9b69 Fix MIP model: make objective function linear to avoid multiplication error 2026-01-08 10:03:49 +08:00
dmy
579f8866c4 Fix MIP toggle bug: handle PuLP import gracefully 2026-01-08 10:01:46 +08:00
dmy
4230d2221d Add MIP module for collector layout optimization 2026-01-08 09:54:40 +08:00
dmy
46e929bfce Implement genetic algorithm for collector layout optimization 2026-01-08 09:46:00 +08:00
dmy
f2a960e789 feat: 优化回路数计算逻辑,提升报表准确性 2026-01-07 16:55:11 +08:00
dmy
87cea6ed86 feat: 优化文件保存对话框并增强系统稳定性
- 重新启用run.io_bound()调用,使用后台线程执行PowerShell脚本
- 为所有文件保存操作添加按钮防重复点击功能
- 新增win32_helper模块,提供Win32 API和COM接口的文件对话框
- 简化导出最佳方案DXF的代码结构
- 改进异步操作和错误处理机制
2026-01-07 12:47:58 +08:00
dmy
e0b5b0c3dc feat: 在方案对比表格中增加损耗费用净现值列
- 新增'损耗费用净现值 (万元)'列,显示生命周期内损耗费用的净现值
- 使用npv_loss字段替代annual_loss_cost,考虑折现率对全生命周期成本的影响
- 支持按损耗费用净现值排序,便于方案经济性对比
2026-01-07 11:27:29 +08:00
dmy
7aef58de1e fix: 修正损耗计算单位从瓦特(W)转换为千瓦(kW)
- 将evaluate_design函数中的损耗计算结果从W转换为kW
- loss_w变量存储三相损耗(W),loss_kw转换为kW后累加
- 确保total_loss返回值单位为kW,与后续经济性分析计算一致
2026-01-07 10:01:32 +08:00
dmy
45c99b41b3 fix: 移除 native 模式下的 run.io_bound() 调用
- 在 native=True 模式下不能使用 run.io_bound() 执行 CPU 密集型任务
- 将 PowerShell 调用改为同步执行 subprocess.run()
- 解决 'Unable to run CPU-bound in script mode' 错误
2026-01-07 01:40:42 +08:00
dmy
837158270e fix: 优化文件保存对话框并启用原生窗口模式
- 添加 matplotlib.use('Agg') 设置非交互式后端
- 重构 save_file_with_dialog 函数,使用 PowerShell 原生对话框替代 Tkinter
- 解决 PyWebview/Tkinter 线程冲突导致的 PicklingError 问题
- 启用 native=True 原生窗口模式,提供更好的用户体验
2026-01-07 01:03:46 +08:00
dmy
61fa870778 feat: 完善经济性分析功能和优化界面显示
- 新增工程运行期限、折现率、年损耗小时数参数配置
- 实现总费用计算功能(包含电缆投资NPV和电费损耗NPV)
- 修复total_investment函数调用时机问题,确保GUI模式正确计算
- 优化电缆单价显示为万元/km单位
- 总长度显示单位改为公里
- 方案对比结果新增总费用列,支持全生命周期成本比选
- 代码格式化和导入顺序优化
- 添加IFLOW.md项目上下文文档
2026-01-06 15:09:52 +08:00
dmy
c54ad369a4 feat: 新增电价参数配置功能
- 在Excel模板中新增电价参数项(默认0.4元/kWh)
- GUI界面显示电价参数,支持从Excel读取
- 核心计算逻辑集成电价参数,为后续经济性分析做准备
- 支持自定义电价或使用默认值
2026-01-06 11:43:41 +08:00
dmy
86e0e21b58 feat: 方案对比结果表格新增总长度列
- 在方案对比结果中新增'总长度(m)'列
- 自动计算每个方案的海缆总长度
- 支持按总长度排序,方便方案比选
2026-01-06 11:25:43 +08:00
dmy
60a9a57cee feat: 方案对比结果区域改为可折叠显示
- 将方案对比结果卡片改为可折叠的expansion组件
- 添加analytics图标,提升界面美观度
- 默认展开状态,方便用户查看
- 优化卡片样式,移除内边距并添加overflow-hidden
2026-01-06 11:16:53 +08:00
dmy
db6114ef57 feat: 优化电缆规格显示和更新项目配置
- GUI电缆规格表格新增'是否为可选'列,支持显示可选电缆标识
- 修复信息容器初始化问题,确保提示文本正确显示
- 更新使用说明文档,修正适用对象和技术支持信息
- 添加项目配置文件(.gitignore, .python-version, pyproject.toml, uv.lock)
- 添加版本管理脚本(make_version.py)
- 添加Excel数据文件和使用说明PDF文档
2026-01-06 10:01:11 +08:00
dmy
67b1f55b92 build: 优化构建流程,添加版本管理和更新文档
- 更新Makefile: 改用nicegui-pack进行打包,添加自动版本号生成
- 添加build.spec和海上风电场集电线路设计优化系统.spec配置文件
- GUI集成版本号显示,支持动态版本管理
- 更新使用说明文档,补充系统概述和修正导出说明
2026-01-06 08:45:57 +08:00
dmy
b924f75add docs: 完善项目文档和使用说明
- 更新README.md,补充GUI特性说明和参数配置文档
- 新增使用说明目录,包含完整的操作手册和界面截图
- 优化generate_template.py,支持自定义模板输出路径
- 改进GUI界面布局,优化文件上传和模板导出功能
- 添加系统参数配置说明(电压、功率因数等)
2026-01-05 23:30:29 +08:00
dmy
c6168afd1d refactor: 调整GUI界面布局宽度为全屏显示 2026-01-05 22:27:15 +08:00
dmy
9352005db3 更新了gui界面 2026-01-05 22:06:03 +08:00
dmy
15d8f4881d feat: 改进文件保存对话框,支持跨平台系统原生保存
主要改进:
1. 新增 save_file_with_dialog 函数
   - 优先使用 PyWebview 原生模式保存对话框
   - 回退到 Tkinter 对话框(本地环境)
   - 最终回退到浏览器下载方式

2. 优化所有导出功能
   - Excel 对比表导出支持系统保存对话框
   - DXF 文件导出支持系统保存对话框
   - ZIP 批量导出支持系统保存对话框
   - 模板导出支持系统保存对话框

3. 代码质量改进
   - 统一异步函数命名规范(on_click_*)
   - 改进代码格式化和缩进
   - 添加详细的调试日志

4. 用户体验提升
   - 用户可以自由选择保存位置
   - 支持文件类型过滤
   - 自动处理文件名后缀
2026-01-05 21:32:46 +08:00
dmy
751bdef245 feat: 优化GUI用户体验和打包配置
主要改进:
1. GUI界面优化
   - 自定义文件上传显示组件,替换默认列表为更美观的卡片式展示
   - 支持环境变量 PROJECT_TEMP_DIR 配置临时目录路径
   - 优化文件导出路径管理,统一使用临时目录
   - 改进端口查找逻辑,从8082开始避免常用端口冲突
   - 修复打包后无控制台模式的stdout/stderr处理

2. 打包配置改进
   - 更新Makefile使用.spec文件进行打包
   - 添加nicegui-pack打包选项
   - 优化clean命令,使用Python跨平台清理

3. 代码优化
   - 注释掉main.py中的详细统计信息打印
   - 改进打包环境的日志配置方式
2026-01-05 17:09:39 +08:00
dmy
05ac7a3388 refactor: 统一导出文件命名规则,使用文件前缀变量
提取file_prefix变量统一管理导出文件的命名前缀,
确保所有导出的Excel和DXF文件使用一致的命名规则,
提升代码可维护性和文件命名的一致性。
2026-01-05 10:35:09 +08:00
dmy
f28e087cd2 feat: 添加Makefile用于PyInstaller打包
提供便捷的打包命令:make build/rebuild/clean/help
使用uv run确保在虚拟环境中执行pyinstaller命令
2026-01-05 10:14:40 +08:00
dmy
a5b46529da fix: 修复打包后exe程序在无控制台模式下运行时的uvicorn日志配置错误
通过检测sys.frozen判断运行环境,只在打包后的exe程序中禁用日志配置,
避免AttributeError: 'NoneType' object has no attribute 'isatty'错误。
普通Python运行环境保留完整日志功能,方便调试。
2026-01-05 09:52:51 +08:00
dmy
2ec763b86a feat: 增强电缆数据校验和UI优化 2026-01-04 19:11:45 +08:00
dmy
a42a8ec7f1 feat: 增强日志显示和方案表格信息展示 2026-01-04 18:56:00 +08:00
dmy
dd7265ff4f feat: 导出全部方案时自动包含Excel报表 2026-01-04 18:40:48 +08:00
dmy
3f73a9be26 feat: 增强导出功能和端口自动分配 2026-01-04 18:33:34 +08:00
dmy
369430aa67 feat: 优化GUI推荐方案选择和用户交互 2026-01-04 17:39:09 +08:00
dmy
00d480edbb refactor: 代码格式统一和Excel导出优化 2026-01-04 14:01:16 +08:00
dmy
06680a6e33 fix: 修复GUI界面图表显示和事件处理问题 2026-01-04 12:05:06 +08:00
dmy
6f2f851a6e feat: 新增Web GUI界面,支持交互式设计对比 2026-01-04 11:53:15 +08:00
29 changed files with 6674 additions and 706 deletions

14
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1 @@
cpython-3.12.12-windows-x86_64-none

16
.vscode/launch.json vendored Normal file
View 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"
},
]
}

240
IFLOW.md Normal file
View 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.3CAD导出
- 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
View 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

151
README.md
View File

@@ -1,116 +1,91 @@
# 海上风电场集电系统设计工具 # 海上风电场集电系统设计优化工具 (Wind Farm Collector System Optimizer)
一个用于设计和优化海上风电场集电系统的Python工具支持多种布局算法和电缆优化方案。 一个用于设计和优化海上风电场集电系统拓扑的综合工具支持多种先进算法,能够根据风机坐标、功率以及海缆规格,自动生成投资成本最低、损耗最小的设计方案。
## 功能特性 ## 🌟 主要功能
- 🌊 多种风机布局生成(随机分布、规则网格) - 🖥️ **原生桌面体验**:支持 Native 模式运行,提供类似本地应用的流畅体验(基于 NiceGUI & PyWebview
- 🔌 多种集电系统设计算法: - 🌊 **多种布局生成**:内置模拟数据生成器,支持规则网格和随机分布布局。
- 最小生成树MST算法 - 🔌 **先进设计算法**
- K-means聚类算法 - **MST (Minimum Spanning Tree)**:无容量约束基准方案。
- 容量扫描算法(Capacitated Sweep - **Capacitated Sweep (Base)**:基础扇区扫描分组。
- 旋转优化算法(Rotational Sweep - **Rotational Sweep**:全局最优起始角度旋转扫描优化。
- 📊 多方案对比分析和可视化 - **Esau-Williams**:经典启发式算法,在距离与容量间寻找最优平衡。
- 📋 自动导出DXF图纸和Excel报告 - ⚙️ **灵活参数配置**:支持通过 Excel 自定义系统电压、功率因数及详细电缆规格。
- 🔧 智能电缆规格选择和成本优化 - 📊 **智能方案对比**:自动运行三大场景(标准方案、含可选电缆方案、限制最大截面方案)并对比结果。
- 📁 **多格式导出**
- 自动生成 CAD 图纸 (`.dxf`),按电缆规格分层并着色。
- 导出详细的 Excel 对比报告及单方案电缆清册。
- 支持一键打包导出所有方案压缩包 (`.zip`)。
## 安装依赖 ## 🛠️ 安装依赖
本项目使用 `uv``pip` 管理环境。推荐安装依赖:
```bash ```bash
pip install numpy pandas matplotlib scikit-learn scipy networkx pip install numpy pandas matplotlib scikit-learn scipy networkx ezdxf nicegui openpyxl pywebview
``` ```
*注:`pywebview` 用于支持原生窗口模式。*
## 使用方法 ## 🚀 使用方法
### 基本用法 ### 1. 启动图形化界面 (推荐)
运行以下命令启动应用,程序将自动弹出独立窗口:
```bash ```bash
python main.py python gui.py
``` ```
### 指定数据文件 **GUI 特性:**
- **模板导出**:点击界面上的 "导出 Excel 模板" 按钮可获取标准输入格式文件。
- **参数概览**:上传文件后自动解析并显示系统参数及电缆规格。
- **交互式分析**:点击结果表格中的行,可实时查看对应方案的拓扑图。
### 2. 命令行模式
```bash ```bash
python main.py --excel wind_farm_coordinates.xlsx python main.py --excel your_data.xlsx
``` ```
### 覆盖默认簇数 ## 📝 输入数据规范 (Excel)
```bash 为了确保计算结果的准确性,输入 Excel 文件应包含以下 Sheet推荐使用 GUI 导出模板):
python main.py --clusters 20
```
## 算法说明 ### 1. Coordinates (坐标)
| Type | ID | X | Y | Power | PlatformHeight |
|------|----|---|---|-------|----------------|
| Substation | Sub1 | 4000 | -800 | 0 | 0 |
| Turbine | 1 | 0 | 0 | 8.0 | 25 |
| ... | ... | ... | ... | ... | ... |
### 1. MST Method最小生成树 ### 2. Cables (电缆)
- 使用最小生成树连接所有风机到海上变电站 **必须遵守以下规则:**
- 简单高效,适合初步设计 - **单调递增性**:电缆必须按截面从小到大排列,且对应的额定载流量也必须严格递增。
- **可选电缆规则**
- `Optional` 列标记为 'Y' 的电缆最多只能有一条。
- 若存在可选电缆,它必须是列表中截面最大的一条。
### 2. K-means Clustering ### 3. Parameters (参数) [可选]
- 将风机分组到多个回路中 可自定义系统级参数,若不提供则使用默认值。
- 平衡每回路的功率分配
### 3. Capacitated Sweep容量扫描 | Parameter | Value | 说明 |
- 考虑电缆容量约束的智能分组 |-----------|-------|------|
- 支持多种电缆规格自动选择 | Voltage (kV) | 66 | 系统电压,支持 `Voltage``System Voltage`。若 key 包含 `kV` 则自动 *1000。 |
| Power Factor | 0.95 | 功率因数 (0-1)。 |
### 4. Rotational Sweep旋转优化 ## 📈 场景说明 (Scenarios)
- 在容量扫描基础上进行旋转优化
- 进一步降低总成本和损耗
## 输出文件 1. **Scenario 1 (Standard)**:仅使用非可选(标准)电缆进行优化。
2. **Scenario 2 (With Optional)**:包含标记为 'Y' 的大型电缆,适用于尝试增加单回路容量的场景。
3. **Scenario 3 (No Max)**:排除最大截面电缆,测试在电缆供应受限时的最优拓扑。
1. **可视化图片**`wind_farm_design_comparison.png` ## 📂 输出文件说明
- 不同算法的设计方案对比图
2. **CAD图纸**`wind_farm_design.dxf` - **Excel 报告**`[文件名]_result.xlsx` 包含所有方案的总览及详细连接清单。
- 可导入CAD软件的详细设计图纸 - **CAD 图纸**`design_[方案名].dxf` 包含分层分色的拓扑图。
- **全部方案**`[文件名]_result.zip` 包含所有图纸及 Excel 报告。
3. **数据报告**`wind_farm_design.xlsx` ## ⚖️ 许可证
- 包含所有方案的详细技术参数和成本分析
## 关键参数说明 本项目仅供工程学习、研究和初步设计评估使用。详细计算应以专业设计院规范为准。
可以在 `main.py` 中调整以下核心常量以适配不同项目:
```python
VOLTAGE_LEVEL = 66000 # 集电系统电压 (V)
POWER_FACTOR = 0.95 # 功率因数
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
===== 开始比较电缆方案 =====
--- All Cables (Base) ---
[Base] Cost: ¥12,456,789.12 | Loss: 234.56 kW
[Rotational] Cost: ¥12,234,567.89 | Loss: 223.45 kW
--- 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) (默认)
```
## 许可证
本项目仅供学习和研究使用。

87
build.spec Normal file
View 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,
)

193
ga.py Normal file
View 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

View File

@@ -1,7 +1,7 @@
import pandas as pd import pandas as pd
import numpy as np import numpy as np
def create_template(): def create_template(output_file='windfarm_template.xlsx'):
# Create sample data similar to the internal generator # Create sample data similar to the internal generator
data = [] data = []
@@ -38,24 +38,35 @@ def create_template():
# Create Cable data # Create Cable data
cable_data = [ cable_data = [
{'CrossSection': 35, 'Capacity': 150, 'Resistance': 0.524, 'Cost': 80, 'Optional': ''}, {'CrossSection': 35, 'Capacity': 150, 'Resistance': 0.524, 'Cost': 8, 'Optional': ''},
{'CrossSection': 70, 'Capacity': 215, 'Resistance': 0.268, 'Cost': 120, 'Optional': ''}, {'CrossSection': 70, 'Capacity': 215, 'Resistance': 0.268, 'Cost': 12, 'Optional': ''},
{'CrossSection': 95, 'Capacity': 260, 'Resistance': 0.193, 'Cost': 150, 'Optional': ''}, {'CrossSection': 95, 'Capacity': 260, 'Resistance': 0.193, 'Cost': 15, 'Optional': ''},
{'CrossSection': 120, 'Capacity': 295, 'Resistance': 0.153, 'Cost': 180, 'Optional': ''}, {'CrossSection': 120, 'Capacity': 295, 'Resistance': 0.153, 'Cost': 18, 'Optional': ''},
{'CrossSection': 150, 'Capacity': 330, 'Resistance': 0.124, 'Cost': 220, 'Optional': ''}, {'CrossSection': 150, 'Capacity': 330, 'Resistance': 0.124, 'Cost': 22, 'Optional': ''},
{'CrossSection': 185, 'Capacity': 370, 'Resistance': 0.0991, 'Cost': 270, 'Optional': ''}, {'CrossSection': 185, 'Capacity': 370, 'Resistance': 0.0991, 'Cost': 27, 'Optional': ''},
{'CrossSection': 240, 'Capacity': 425, 'Resistance': 0.0754, 'Cost': 350, 'Optional': ''}, {'CrossSection': 240, 'Capacity': 425, 'Resistance': 0.0754, 'Cost': 35, 'Optional': ''},
{'CrossSection': 300, 'Capacity': 500, 'Resistance': 0.0601, 'Cost': 450, 'Optional': ''}, {'CrossSection': 300, 'Capacity': 500, 'Resistance': 0.0601, 'Cost': 45, 'Optional': ''},
{'CrossSection': 400, 'Capacity': 580, 'Resistance': 0.0470, 'Cost': 600, 'Optional': ''} {'CrossSection': 400, 'Capacity': 580, 'Resistance': 0.0470, 'Cost': 60, 'Optional': ''}
] ]
df_cables = pd.DataFrame(cable_data) 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 # Save to Excel
output_file = 'coordinates.xlsx'
with pd.ExcelWriter(output_file) as writer: with pd.ExcelWriter(output_file) as writer:
df.to_excel(writer, sheet_name='Coordinates', index=False) df.to_excel(writer, sheet_name='Coordinates', index=False)
df_cables.to_excel(writer, sheet_name='Cables', index=False) df_cables.to_excel(writer, sheet_name='Cables', index=False)
print(f"Created sample file: {output_file} with sheets 'Coordinates' and 'Cables'") 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__": if __name__ == "__main__":
create_template() create_template()

1182
gui.py Normal file

File diff suppressed because it is too large Load Diff

1954
main.py

File diff suppressed because it is too large Load Diff

55
make_version.py Normal file
View 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
View 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

25
pyproject.toml Normal file
View 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
View 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
View 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()

2290
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

232
win32_helper.py Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

0
使用说明/pandoc Normal file
View File

View 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` 文件。建议在此模板基础上进行修改。
![图示:软件主界面左侧配置面板,高亮“导出 Excel 模板”按钮](导出模板按钮截图.png)
### 2.2 数据表详解
输入文件通常包含三个 Sheet工作表
![图示:输入数据表](输入数据表.png)
#### (1) Coordinates (坐标数据) - **[必须]**
录入升压站和所有风机的位置及参数。
* **Type**: 填写 `Substation` (升压站) 或 `Turbine` (风机)。
* **ID**: 设备编号(如 Sub1, 01, 02...)。
* **X / Y**: 投影坐标(单位:米)。建议使用高斯投影坐标 (X, Y),以保证距离计算准确。
* **Power**: 设备功率单位MW。升压站填 0。
* **PlatformHeight**: 塔筒/升压站平台高度(单位:米),用于计算海缆爬升段长度。
<!-- ![图示Excel中Coordinates工作表的填写范例](占位符-坐标数据表截图) -->
#### (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. 右侧信息面板会自动解析并显示**系统参数**(电压、功率因数)和**电缆规格列表**,请务必核对这些数据是否正确。
![图示:文件上传成功后的状态,以及右侧参数预览面板](数据加载成功截图.png)
### 步骤三:运行计算
点击左侧下方的大型按钮 **“运行方案对比”**。
软件将自动执行以下计算任务:
1. **多场景分析**
* *Scenario 1 (标准)*:仅使用标准电缆库进行优化。
* *Scenario 2 (含可选)*:尝试引入更大截面的可选电缆,评估是否能减少回路数。
* *Scenario 3 (限制)*:模拟最大截面电缆缺货情况下的次优方案。
2. **多算法寻优**:对每个场景同时运行 MST、基础扫描、旋转扫描、Esau-Williams 等多种算法。
*注意:计算过程中下方黑色日志窗口会实时滚动显示计算进度,通常耗时 10-60 秒,取决于风机数量。*
![图示:正在计算时的进度条和日志窗口](计算过程截图.png)
### 步骤四:查看与比选
计算完成后,系统会自动筛选出**综合造价最低**的推荐方案,并在界面上展示。
1. **结果列表**
右侧中部的表格列出了所有计算出的可行方案。
* `Cost (万元)`:总投资估算。
* `Loss (kW)`:全场集电线路总线损。
* **操作**:点击表格中的任意一行,下方的拓扑图会自动切换到该方案。
2. **拓扑可视化**
右侧下方的绘图区展示集电线路走向。
* 不同颜色的线条代表不同截面的海缆。
* 图例会标明线型对应的截面。
* 升压站显示为红色方块,风机为圆点。
![图示:结果对比表格和拓扑图联动展示](结果比选交互截图.png)
---
## 5. 成果导出
比选确定最终方案后,可以使用底部的 **“导出与下载”** 功能区生成设计文件。
### 5.1 导出 CAD 图纸 (.dxf)
* **导出推荐方案**:直接点击 **“导出推荐方案 DXF”**。
* **导出特定方案**:在表格中选中任意一行,点击 **“导出选中方案 DXF”**。
生成的 DXF 文件特点:
* **分层管理**不同截面的电缆位于不同图层Layer方便在 AutoCAD 中通过图层过滤器批量修改线型或颜色。
* **地理坐标**:图纸保留了 Excel 中的原始坐标系,可直接通过“原点粘贴”功能合并到项目总图中。
![图示导出的DXF图纸在CAD中打开的效果](CAD图纸效果示例.png)
### 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) 附近,可能会找不到图形。
---
**技术支持:** 海上能源业务开发部 - 杜孟远

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View 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,
)