Compare commits
28 Commits
b924f75add
...
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 |
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"
|
||||
},
|
||||
]
|
||||
}
|
||||
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日
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目仅供工程学习、研究和初步设计评估使用。详细计算应以专业设计院规范为准。
|
||||
21
Makefile
21
Makefile
@@ -5,27 +5,30 @@ help:
|
||||
@echo "海上风电场集电线路设计优化系统 - 构建脚本"
|
||||
@echo ""
|
||||
@echo "可用命令:"
|
||||
@echo " make build - 使用 .spec 文件生成单文件 exe 程序"
|
||||
@echo " make build - 使用 .spec 文件生成单文件 exe 程序 (包含自动版本号生成)"
|
||||
@echo " make rebuild - 清理并重新构建"
|
||||
@echo " make clean - 清理构建生成的临时文件和缓存"
|
||||
@echo " make clean - 清理编译生成的临时文件和缓存"
|
||||
@echo " make help - 显示此帮助信息"
|
||||
|
||||
# 生成单文件exe程序
|
||||
# 使用 --clean 清理 PyInstaller 缓存,-y 自动覆盖输出
|
||||
# 使用 nicegui-pack 打包
|
||||
build:
|
||||
@echo "开始打包程序..."
|
||||
uv run pyinstaller --clean -y "海上风电场集电线路设计优化系统.spec"
|
||||
@echo "打包完成!"
|
||||
@echo "可执行文件位于: dist/海上风电场集电线路设计优化系统.exe"
|
||||
@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
|
||||
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,
|
||||
)
|
||||
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
|
||||
@@ -38,22 +38,26 @@ def create_template(output_file='windfarm_template.xlsx'):
|
||||
|
||||
# Create Cable data
|
||||
cable_data = [
|
||||
{'CrossSection': 35, 'Capacity': 150, 'Resistance': 0.524, 'Cost': 80, 'Optional': ''},
|
||||
{'CrossSection': 70, 'Capacity': 215, 'Resistance': 0.268, 'Cost': 120, 'Optional': ''},
|
||||
{'CrossSection': 95, 'Capacity': 260, 'Resistance': 0.193, 'Cost': 150, 'Optional': ''},
|
||||
{'CrossSection': 120, 'Capacity': 295, 'Resistance': 0.153, 'Cost': 180, 'Optional': ''},
|
||||
{'CrossSection': 150, 'Capacity': 330, 'Resistance': 0.124, 'Cost': 220, 'Optional': ''},
|
||||
{'CrossSection': 185, 'Capacity': 370, 'Resistance': 0.0991, 'Cost': 270, 'Optional': ''},
|
||||
{'CrossSection': 240, 'Capacity': 425, 'Resistance': 0.0754, 'Cost': 350, 'Optional': ''},
|
||||
{'CrossSection': 300, 'Capacity': 500, 'Resistance': 0.0601, 'Cost': 450, 'Optional': ''},
|
||||
{'CrossSection': 400, 'Capacity': 580, 'Resistance': 0.0470, 'Cost': 600, 'Optional': ''}
|
||||
{'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': '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)
|
||||
|
||||
|
||||
636
gui.py
636
gui.py
@@ -1,20 +1,31 @@
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import contextlib
|
||||
import tempfile
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.backends.backend_svg
|
||||
from nicegui import ui, events, app
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
from nicegui import app, events, ui, run
|
||||
|
||||
from main import (
|
||||
compare_design_methods,
|
||||
export_to_dxf,
|
||||
load_data_from_excel,
|
||||
generate_wind_farm_data,
|
||||
visualize_design,
|
||||
export_all_scenarios_to_excel,
|
||||
export_to_dxf,
|
||||
generate_wind_farm_data,
|
||||
load_data_from_excel,
|
||||
visualize_design,
|
||||
)
|
||||
import pandas as pd
|
||||
|
||||
# 尝试导入自动生成的版本号
|
||||
try:
|
||||
from version import VERSION
|
||||
except ImportError:
|
||||
VERSION = "v1.0"
|
||||
|
||||
# 设置matplotlib支持中文显示
|
||||
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "Arial"]
|
||||
@@ -79,6 +90,9 @@ def index():
|
||||
"run_btn": None,
|
||||
"current_file_container": None, # 替换 label 为 container
|
||||
"info_container": None, # 新增信息展示容器
|
||||
"ga_switch": None, # 遗传算法开关
|
||||
"mip_switch": None, # MIP开关
|
||||
"log_content": "", # 存储计算日志内容
|
||||
}
|
||||
|
||||
def update_info_panel():
|
||||
@@ -102,7 +116,7 @@ def index():
|
||||
v = state["system_params"]["voltage"]
|
||||
is_default_v = False
|
||||
|
||||
v_str = f"电压: {v/1000:.1f} kV" if v >= 1000 else f"电压: {v} V"
|
||||
v_str = f"电压: {v / 1000:.1f} kV" if v >= 1000 else f"电压: {v} V"
|
||||
if is_default_v:
|
||||
v_str += " (默认)"
|
||||
params_text.append(v_str)
|
||||
@@ -122,6 +136,66 @@ def index():
|
||||
pf_str += " (默认)"
|
||||
params_text.append(pf_str)
|
||||
|
||||
# 获取电价
|
||||
ep = 0.4 # Default
|
||||
is_default_ep = True
|
||||
if (
|
||||
state.get("system_params")
|
||||
and "electricity_price" in state["system_params"]
|
||||
):
|
||||
ep = state["system_params"]["electricity_price"]
|
||||
is_default_ep = False
|
||||
|
||||
ep_str = f"电价: {ep} 元/kWh"
|
||||
if is_default_ep:
|
||||
ep_str += " (默认)"
|
||||
params_text.append(ep_str)
|
||||
|
||||
# 获取工程运行期限
|
||||
lifetime = 25 # Default
|
||||
is_default_lifetime = True
|
||||
if (
|
||||
state.get("system_params")
|
||||
and "project_lifetime" in state["system_params"]
|
||||
):
|
||||
lifetime = state["system_params"]["project_lifetime"]
|
||||
is_default_lifetime = False
|
||||
|
||||
lifetime_str = f"工程运行期限: {lifetime} 年"
|
||||
if is_default_lifetime:
|
||||
lifetime_str += " (默认)"
|
||||
params_text.append(lifetime_str)
|
||||
|
||||
# 获取折现率
|
||||
discount_rate = 8 # Default
|
||||
is_default_discount = True
|
||||
if (
|
||||
state.get("system_params")
|
||||
and "discount_rate" in state["system_params"]
|
||||
):
|
||||
discount_rate = state["system_params"]["discount_rate"]
|
||||
is_default_discount = False
|
||||
|
||||
discount_str = f"折现率: {discount_rate}%"
|
||||
if is_default_discount:
|
||||
discount_str += " (默认)"
|
||||
params_text.append(discount_str)
|
||||
|
||||
# 获取年损耗小时数
|
||||
annual_hours = 1400 # Default
|
||||
is_default_hours = True
|
||||
if (
|
||||
state.get("system_params")
|
||||
and "annual_loss_hours" in state["system_params"]
|
||||
):
|
||||
annual_hours = state["system_params"]["annual_loss_hours"]
|
||||
is_default_hours = False
|
||||
|
||||
hours_str = f"年损耗小时数: {annual_hours} 小时"
|
||||
if is_default_hours:
|
||||
hours_str += " (默认)"
|
||||
params_text.append(hours_str)
|
||||
|
||||
for p in params_text:
|
||||
ui.chip(p, icon="bolt").props("outline color=primary")
|
||||
|
||||
@@ -154,10 +228,16 @@ def index():
|
||||
},
|
||||
{
|
||||
"name": "cost",
|
||||
"label": "参考单价 (元/m)",
|
||||
"label": "参考单价(万元/km)",
|
||||
"field": "cost",
|
||||
"align": "center",
|
||||
},
|
||||
{
|
||||
"name": "is_optional",
|
||||
"label": "是否为可选",
|
||||
"field": "is_optional",
|
||||
"align": "center",
|
||||
},
|
||||
]
|
||||
rows = []
|
||||
for spec in state["cable_specs"]:
|
||||
@@ -167,7 +247,10 @@ def index():
|
||||
"section": spec[0],
|
||||
"capacity": spec[1],
|
||||
"resistance": spec[2],
|
||||
"cost": spec[3],
|
||||
"cost": f"{spec[3] / 10:.2f}"
|
||||
if spec[3] is not None
|
||||
else "0.00",
|
||||
"is_optional": "Y" if len(spec) > 4 and spec[4] else "",
|
||||
}
|
||||
)
|
||||
ui.table(columns=columns, rows=rows).classes("w-full").props(
|
||||
@@ -260,6 +343,16 @@ def index():
|
||||
|
||||
update_info_panel()
|
||||
|
||||
# 清空方案对比结果和拓扑可视化
|
||||
state["results"] = []
|
||||
if refs["results_table"]:
|
||||
refs["results_table"].rows = []
|
||||
refs["results_table"].selected = []
|
||||
if refs["plot_container"]:
|
||||
refs["plot_container"].clear()
|
||||
if refs["export_row"]:
|
||||
refs["export_row"].clear()
|
||||
|
||||
# 清空上传组件列表,以便下次选择(配合 .no-list CSS 使用)
|
||||
if refs["upload_widget"]:
|
||||
refs["upload_widget"].reset()
|
||||
@@ -267,7 +360,9 @@ def index():
|
||||
except Exception as ex:
|
||||
ui.notify(f"上传处理失败: {ex}", type="negative")
|
||||
|
||||
async def save_file_with_dialog(filename, callback, file_filter="All files (*.*)"):
|
||||
async def save_file_with_dialog(
|
||||
filename, callback, file_filter="All files (*.*)", sender=None
|
||||
):
|
||||
"""
|
||||
跨平台文件保存助手。
|
||||
如果是原生模式,弹出系统保存对话框。
|
||||
@@ -276,136 +371,84 @@ def index():
|
||||
:param filename: 默认文件名
|
||||
:param callback: 接收文件路径并执行保存操作的函数 (filepath) -> None
|
||||
:param file_filter: 格式如 "Excel Files (*.xlsx)"
|
||||
:param sender: 触发该操作的 UI 组件,用于在操作期间禁用以防重复点击
|
||||
"""
|
||||
# 检测是否为原生模式 (PyWebview)
|
||||
is_native = False
|
||||
native_window = None
|
||||
if sender:
|
||||
sender.disable()
|
||||
try:
|
||||
# 使用 getattr 安全获取 app.native,避免属性不存在错误
|
||||
# 并在 reload=True 时 native 可能未能正确初始化
|
||||
n_obj = getattr(app, "native", None)
|
||||
if n_obj and getattr(n_obj, "main_window", None):
|
||||
is_native = True
|
||||
native_window = n_obj.main_window
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Native check error: {e}")
|
||||
# 方案:使用 PowerShell 弹出原生保存对话框 (仅限 Windows)
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
print(
|
||||
f"DEBUG: save_file_with_dialog called. is_native={is_native}, filename={filename}"
|
||||
)
|
||||
|
||||
if is_native and native_window:
|
||||
try:
|
||||
# PyWebview 的 create_file_dialog 的 file_types 参数期望一个字符串元组
|
||||
# 格式如: ('Description (*.ext)', 'All files (*.*)')
|
||||
file_types = (file_filter,)
|
||||
|
||||
print(f"DEBUG: calling create_file_dialog with types={file_types}")
|
||||
|
||||
# 在 Native 模式下,create_file_dialog 是同步阻塞的
|
||||
# 注意:必须使用 app.native.SAVE_DIALOG
|
||||
save_path = native_window.create_file_dialog(
|
||||
app.native.SAVE_DIALOG,
|
||||
directory="",
|
||||
save_filename=filename,
|
||||
file_types=file_types,
|
||||
)
|
||||
|
||||
print(f"DEBUG: save_path result: {save_path}")
|
||||
|
||||
# 用户取消
|
||||
if not save_path:
|
||||
return
|
||||
|
||||
# 处理返回类型 (PyWebview 可能返回字符串或列表)
|
||||
if isinstance(save_path, (list, tuple)):
|
||||
if not save_path:
|
||||
return
|
||||
save_path = save_path[0]
|
||||
|
||||
# 确保文件名后缀正确
|
||||
if not save_path.lower().endswith(
|
||||
os.path.splitext(filename)[1].lower()
|
||||
):
|
||||
save_path += os.path.splitext(filename)[1]
|
||||
|
||||
await callback(save_path)
|
||||
ui.notify(f"文件已保存至: {save_path}", type="positive")
|
||||
return # 成功处理,退出
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
print(f"ERROR in save_file_with_dialog (native): {e}")
|
||||
# ui.notify(f"原生保存失败,尝试其他方式: {e}", type="warning")
|
||||
print(f"原生保存失败,尝试其他方式: {e}")
|
||||
# 继续向下执行,尝试 fallback
|
||||
|
||||
# 非 Native 模式 (或 Native 失败),尝试使用 Tkinter (仅限本地环境)
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
from nicegui import run
|
||||
|
||||
print("DEBUG: Attempting Tkinter dialog...")
|
||||
|
||||
def get_save_path_tk(default_name, f_filter):
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
# 创建隐藏的根窗口
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
root.attributes("-topmost", True) # 尝试置顶
|
||||
# 构建 PowerShell 脚本
|
||||
# 注意:过滤器格式为 "描述|*.ext|所有文件|*.*"
|
||||
ps_filter = file_filter.replace("(", "|").replace(")", "")
|
||||
if "|" not in ps_filter:
|
||||
ps_filter += f"|{os.path.splitext(filename)[1] or '*.*'}"
|
||||
|
||||
# 转换 filter 格式: "Excel Files (*.xlsx)" -> [("Excel Files", "*.xlsx")]
|
||||
filetypes = []
|
||||
if "(" in f_filter and ")" in f_filter:
|
||||
desc = f_filter.split("(")[0].strip()
|
||||
ext = f_filter.split("(")[1].split(")")[0]
|
||||
filetypes.append((desc, ext))
|
||||
filetypes.append(("All files", "*.*"))
|
||||
# 简单清洗 filter 字符串以适应 PowerShell (e.g., "Excel Files *.xlsx" -> "Excel Files|*.xlsx")
|
||||
# 这里做一个简化的映射,确保格式正确
|
||||
if "Excel" in file_filter:
|
||||
ps_filter = "Excel Files (*.xlsx)|*.xlsx|All Files (*.*)|*.*"
|
||||
elif "DXF" in file_filter:
|
||||
ps_filter = "DXF Files (*.dxf)|*.dxf|All Files (*.*)|*.*"
|
||||
elif "ZIP" in file_filter:
|
||||
ps_filter = "ZIP Archives (*.zip)|*.zip|All Files (*.*)|*.*"
|
||||
else:
|
||||
ps_filter = "All Files (*.*)|*.*"
|
||||
|
||||
path = filedialog.asksaveasfilename(
|
||||
initialfile=default_name, filetypes=filetypes, title="保存文件"
|
||||
ps_script = f"""
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$d = New-Object System.Windows.Forms.SaveFileDialog
|
||||
$d.Filter = "{ps_filter}"
|
||||
$d.FileName = "{filename}"
|
||||
$d.Title = "保存文件"
|
||||
if ($d.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {{
|
||||
Write-Output $d.FileName
|
||||
}}
|
||||
"""
|
||||
|
||||
# 运行 PowerShell
|
||||
# 使用 startupinfo 隐藏控制台窗口 (防止黑框闪烁)
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
|
||||
print("DEBUG: invoking PowerShell SaveFileDialog...")
|
||||
|
||||
# 使用 run.io_bound 在后台线程执行,避免阻塞主事件循环
|
||||
# 这样按钮的禁用状态可以立即同步到前端
|
||||
result = await run.io_bound(
|
||||
subprocess.run,
|
||||
["powershell", "-Command", ps_script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
startupinfo=startupinfo,
|
||||
)
|
||||
root.destroy()
|
||||
return path
|
||||
except Exception as ex:
|
||||
print(f"Tkinter inner error: {ex}")
|
||||
return None
|
||||
save_path = result.stdout.strip()
|
||||
if save_path:
|
||||
print(f"DEBUG: PowerShell returned path: {save_path}")
|
||||
await callback(save_path)
|
||||
ui.notify(f"文件已保存至: {save_path}", type="positive")
|
||||
return
|
||||
else:
|
||||
print("DEBUG: PowerShell dialog cancelled or empty result.")
|
||||
# 用户取消,直接返回,不回退
|
||||
return
|
||||
|
||||
# 在线程中运行 tkinter,避免阻塞 asyncio 事件循环
|
||||
save_path = await run.io_bound(get_save_path_tk, filename, file_filter)
|
||||
except Exception as e:
|
||||
print(f"PowerShell dialog failed: {e}")
|
||||
# 出错则回退到 ui.download
|
||||
|
||||
if save_path:
|
||||
print(f"DEBUG: Tkinter save_path: {save_path}")
|
||||
# 确保文件名后缀正确
|
||||
if not save_path.lower().endswith(
|
||||
os.path.splitext(filename)[1].lower()
|
||||
):
|
||||
save_path += os.path.splitext(filename)[1]
|
||||
|
||||
await callback(save_path)
|
||||
ui.notify(f"文件已保存至: {save_path}", type="positive")
|
||||
return # 成功处理
|
||||
elif save_path is None:
|
||||
print("DEBUG: Tkinter dialog cancelled or failed silently.")
|
||||
# 如果是用户取消(返回空字符串),通常不需要回退到下载。
|
||||
# 但这里如果 Tkinter 彻底失败返回 None,可能需要回退。
|
||||
# askopenfilename 返回空字符串表示取消。我们假设 None 是异常。
|
||||
# 这里简化处理:只要没拿到路径且没报错,就认为是取消。
|
||||
if save_path == "":
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
print(f"Tkinter dialog failed: {e}")
|
||||
# Fallback to ui.download if tkinter fails
|
||||
|
||||
# 最后的回退方案:浏览器下载
|
||||
print("DEBUG: Falling back to ui.download")
|
||||
temp_path = os.path.join(state["temp_dir"], filename)
|
||||
await callback(temp_path)
|
||||
ui.download(temp_path)
|
||||
# 统一回退方案:浏览器下载
|
||||
print("DEBUG: Using ui.download fallback")
|
||||
temp_path = os.path.join(state["temp_dir"], filename)
|
||||
await callback(temp_path)
|
||||
ui.download(temp_path)
|
||||
finally:
|
||||
if sender:
|
||||
sender.enable()
|
||||
|
||||
def update_export_buttons():
|
||||
if refs["export_row"]:
|
||||
@@ -436,7 +479,6 @@ def index():
|
||||
best_res = min(state["results"], key=lambda x: x["cost"])
|
||||
|
||||
with refs["export_row"]:
|
||||
|
||||
# --- 下载 Excel ---
|
||||
async def save_excel(path):
|
||||
import shutil
|
||||
@@ -448,9 +490,12 @@ def index():
|
||||
# 如果不存在,重新生成
|
||||
export_all_scenarios_to_excel(state["results"], path)
|
||||
|
||||
async def on_click_excel():
|
||||
async def on_click_excel(e):
|
||||
await save_file_with_dialog(
|
||||
default_excel_name, save_excel, "Excel Files (*.xlsx)"
|
||||
default_excel_name,
|
||||
save_excel,
|
||||
"Excel Files (*.xlsx)",
|
||||
sender=e.sender,
|
||||
)
|
||||
|
||||
ui.button(
|
||||
@@ -459,41 +504,7 @@ def index():
|
||||
).props("icon=download")
|
||||
|
||||
# --- 导出推荐方案 DXF ---
|
||||
def export_best_dxf():
|
||||
if state["substation"] is not None:
|
||||
safe_name = "".join(
|
||||
[
|
||||
c
|
||||
for c in best_res["name"]
|
||||
if c.isalnum() or c in (" ", "-", "_")
|
||||
]
|
||||
).strip()
|
||||
default_name = f"{file_prefix}_best_{safe_name}.dxf"
|
||||
|
||||
async def save_dxf(path):
|
||||
export_to_dxf(
|
||||
best_res["turbines"],
|
||||
state["substation"],
|
||||
best_res["eval"]["details"],
|
||||
path,
|
||||
)
|
||||
|
||||
# 包装为 async 任务,并在 NiceGUI 事件循环中执行
|
||||
async def run_save():
|
||||
await save_file_with_dialog(
|
||||
default_name, save_dxf, "DXF Files (*.dxf)"
|
||||
)
|
||||
|
||||
# 这里的 export_best_dxf 本身是普通函数,绑定到 on_click
|
||||
# 但我们需要它执行异步操作。最简单的是让 export_best_dxf 变为 async
|
||||
# 或者在这里直接调用 run_save (但这在普通函数里不行)
|
||||
# 更好的方法是将 export_best_dxf 定义为 async,如下所示
|
||||
return run_save()
|
||||
else:
|
||||
ui.notify("缺少升压站数据,无法导出 DXF", type="negative")
|
||||
|
||||
# 将 export_best_dxf 改为 async 并重命名,以便直接用作回调
|
||||
async def on_click_best_dxf():
|
||||
async def on_click_best_dxf(e):
|
||||
if state["substation"] is not None:
|
||||
safe_name = "".join(
|
||||
[
|
||||
@@ -513,17 +524,17 @@ def index():
|
||||
)
|
||||
|
||||
await save_file_with_dialog(
|
||||
default_name, save_dxf, "DXF Files (*.dxf)"
|
||||
default_name, save_dxf, "DXF Files (*.dxf)", sender=e.sender
|
||||
)
|
||||
else:
|
||||
ui.notify("缺少升压站数据,无法导出 DXF", type="negative")
|
||||
|
||||
ui.button(
|
||||
f'导出推荐方案 DXF ({best_res["name"]})', on_click=on_click_best_dxf
|
||||
f"导出推荐方案 DXF ({best_res['name']})", on_click=on_click_best_dxf
|
||||
).props("icon=architecture color=accent")
|
||||
|
||||
# --- 导出选中方案 DXF ---
|
||||
async def on_click_selected_dxf():
|
||||
async def on_click_selected_dxf(e):
|
||||
if not refs["results_table"] or not refs["results_table"].selected:
|
||||
ui.notify("请先在上方表格中选择一个方案", type="warning")
|
||||
return
|
||||
@@ -553,7 +564,7 @@ def index():
|
||||
)
|
||||
|
||||
await save_file_with_dialog(
|
||||
default_name, save_dxf, "DXF Files (*.dxf)"
|
||||
default_name, save_dxf, "DXF Files (*.dxf)", sender=e.sender
|
||||
)
|
||||
else:
|
||||
ui.notify(
|
||||
@@ -568,7 +579,7 @@ def index():
|
||||
refs["export_selected_btn"].set_text(f"导出选中方案 ({clean_name})")
|
||||
|
||||
# --- 导出全部 ZIP ---
|
||||
async def on_click_all_dxf():
|
||||
async def on_click_all_dxf(e):
|
||||
if not state["results"] or state["substation"] is None:
|
||||
ui.notify("无方案数据可导出", type="warning")
|
||||
return
|
||||
@@ -614,19 +625,78 @@ def index():
|
||||
except:
|
||||
pass
|
||||
|
||||
await save_file_with_dialog(default_name, save_zip, "ZIP Files (*.zip)")
|
||||
await save_file_with_dialog(
|
||||
default_name, save_zip, "ZIP Files (*.zip)", sender=e.sender
|
||||
)
|
||||
|
||||
ui.button("导出全部方案 DXF (ZIP)", on_click=on_click_all_dxf).props(
|
||||
"icon=folder_zip color=secondary"
|
||||
)
|
||||
|
||||
# --- 导出计算日志 ---
|
||||
async def on_click_export_log(e):
|
||||
# 尝试多种方式获取日志内容
|
||||
log_content = ""
|
||||
method_used = "unknown"
|
||||
|
||||
# 方法1: 首先尝试从保存的日志内容获取
|
||||
if refs.get("log_content") and refs["log_content"].strip():
|
||||
log_content = refs["log_content"]
|
||||
method_used = "saved_memory"
|
||||
|
||||
# 方法2: 如果保存的日志为空,尝试使用 JavaScript 获取 log 组件的内容
|
||||
if not log_content.strip() and refs["log_box"]:
|
||||
try:
|
||||
log_id = refs["log_box"].id
|
||||
js_code = f"""
|
||||
(function() {{
|
||||
const logElement = document.querySelector("#c{log_id}");
|
||||
if (logElement) {{
|
||||
console.log("Found log element:", logElement);
|
||||
return logElement.innerText || logElement.textContent || "";
|
||||
}}
|
||||
console.log("Log element not found for ID: c{log_id}");
|
||||
return "";
|
||||
}})()
|
||||
"""
|
||||
result = await ui.run_javascript(js_code)
|
||||
if result and result.strip():
|
||||
log_content = result
|
||||
method_used = "javascript"
|
||||
except Exception as js_error:
|
||||
print(f"JavaScript method failed: {js_error}")
|
||||
|
||||
if not log_content.strip():
|
||||
ui.notify(
|
||||
"没有可导出的日志内容。请先运行计算任务。", type="warning"
|
||||
)
|
||||
print(f"Log export failed. Method tried: {method_used}")
|
||||
return
|
||||
|
||||
print(
|
||||
f"Successfully exported log using method: {method_used}, length: {len(log_content)}"
|
||||
)
|
||||
default_name = f"{file_prefix}_calculation_log.txt"
|
||||
|
||||
async def save_log(path):
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(log_content)
|
||||
|
||||
await save_file_with_dialog(
|
||||
default_name, save_log, "Text Files (*.txt)", sender=e.sender
|
||||
)
|
||||
|
||||
ui.button("导出计算日志", on_click=on_click_export_log).props(
|
||||
"icon=description color=info"
|
||||
)
|
||||
|
||||
def update_plot(result):
|
||||
if refs["plot_container"]:
|
||||
refs["plot_container"].clear()
|
||||
with refs["plot_container"]:
|
||||
# 使用 ui.pyplot 上下文自动管理 figure 生命周期
|
||||
with ui.pyplot(figsize=(10, 8)) as plot:
|
||||
title = f"{result['name']}\nCost: ¥{result['cost']/10000:.2f}万 | Loss: {result['loss']:.2f} kW"
|
||||
title = f"{result['name']}\nCost: ¥{result['cost'] / 10000:.2f}万 | Loss: {result['loss']:.2f} kW"
|
||||
# 显式获取当前 ui.pyplot 创建的 axes,并传递给绘图函数
|
||||
# 确保绘图发生在正确的 figure 上
|
||||
ax = plt.gca()
|
||||
@@ -666,7 +736,6 @@ def index():
|
||||
clean_name = row_name.replace("(推荐) ", "")
|
||||
refs["export_selected_btn"].set_text(f"导出选中方案 ({clean_name})")
|
||||
|
||||
from nicegui import run
|
||||
import queue
|
||||
|
||||
async def run_analysis():
|
||||
@@ -675,8 +744,15 @@ def index():
|
||||
return
|
||||
if refs["log_box"]:
|
||||
refs["log_box"].clear()
|
||||
# 重置日志内容
|
||||
refs["log_content"] = ""
|
||||
log_queue = queue.Queue()
|
||||
|
||||
# 获取开关状态
|
||||
use_ga = refs["ga_switch"].value if refs["ga_switch"] else False
|
||||
use_mip = refs["mip_switch"].value if refs["mip_switch"] else False
|
||||
print(f"Switch values: GA={use_ga}, MIP={use_mip}")
|
||||
|
||||
class QueueLogger(io.StringIO):
|
||||
def write(self, message):
|
||||
if message and message.strip():
|
||||
@@ -690,13 +766,15 @@ def index():
|
||||
try:
|
||||
msg = log_queue.get_nowait()
|
||||
refs["log_box"].push(msg)
|
||||
# 同时保存到日志内容中
|
||||
refs["log_content"] += msg + "\n"
|
||||
new_msg = True
|
||||
if msg.startswith("--- Scenario"):
|
||||
scenario_name = msg.replace("---", "").strip()
|
||||
if refs["status_label"]:
|
||||
refs["status_label"].text = (
|
||||
f"正在计算: {scenario_name}..."
|
||||
)
|
||||
refs[
|
||||
"status_label"
|
||||
].text = f"正在计算: {scenario_name}..."
|
||||
elif "开始比较电缆方案" in msg:
|
||||
if refs["status_label"]:
|
||||
refs["status_label"].text = "准备开始计算..."
|
||||
@@ -724,6 +802,8 @@ def index():
|
||||
n_clusters_override=None,
|
||||
interactive=False,
|
||||
plot_results=False,
|
||||
use_ga=use_ga,
|
||||
use_mip=use_mip,
|
||||
)
|
||||
|
||||
# 在后台线程运行计算任务
|
||||
@@ -749,7 +829,7 @@ def index():
|
||||
|
||||
update_plot(best_res)
|
||||
ui.notify(
|
||||
f'计算完成!已自动加载推荐方案: {best_res["name"]}', type="positive"
|
||||
f"计算完成!已自动加载推荐方案: {best_res['name']}", type="positive"
|
||||
)
|
||||
|
||||
# 更新结果表格
|
||||
@@ -785,10 +865,25 @@ def index():
|
||||
elif "No Max" in original_name:
|
||||
note += "不包含可选电缆型号,且可使用的最大截面电缆降一档截面。"
|
||||
|
||||
# 计算总长度(转换为公里)
|
||||
total_length_m = sum(d["length"] for d in res["eval"]["details"])
|
||||
total_length_km = total_length_m / 1000
|
||||
|
||||
# 获取回路数 (通过统计从升压站发出的连接)
|
||||
n_circuits = sum(
|
||||
1
|
||||
for d in res["eval"]["details"]
|
||||
if d["source"] == "substation" or d["target"] == "substation"
|
||||
)
|
||||
|
||||
row_dict = {
|
||||
"name": name_display,
|
||||
"n_circuits": n_circuits,
|
||||
"cost_wan": f"{res['cost'] / 10000:.2f}",
|
||||
"loss_kw": f"{res['loss']:.2f}",
|
||||
"total_length": f"{total_length_km:.2f}",
|
||||
"npv_loss_wan": f"{res.get('npv_loss', 0) / 10000:.2f}",
|
||||
"total_cost_npv_wan": f"{res.get('total_cost_npv', res['cost']) / 10000:.2f}",
|
||||
"note": note,
|
||||
"original_name": res["name"],
|
||||
}
|
||||
@@ -834,7 +929,7 @@ def index():
|
||||
"bg-primary text-white p-4 shadow-lg items-center no-wrap"
|
||||
):
|
||||
with ui.column().classes("gap-0"):
|
||||
ui.label("海上风电场集电线路设计优化系统 v1.0").classes(
|
||||
ui.label(f"海上风电场集电线路设计优化系统 {VERSION}").classes(
|
||||
"text-2xl font-bold"
|
||||
)
|
||||
with ui.column().classes("gap-0"):
|
||||
@@ -849,14 +944,18 @@ def index():
|
||||
ui.label("配置面板").classes("text-xl font-semibold mb-4 border-b pb-2")
|
||||
|
||||
# 使用 items-stretch 确保所有子元素高度一致
|
||||
with ui.row().classes('w-full items-stretch gap-4'):
|
||||
with ui.row().classes("w-full items-stretch gap-4"):
|
||||
# 1. 导出模板按钮
|
||||
async def export_template():
|
||||
from generate_template import create_template
|
||||
async def export_template(e):
|
||||
import shutil
|
||||
|
||||
from generate_template import create_template
|
||||
|
||||
async def save_template(path):
|
||||
# 生成模板到系统临时目录
|
||||
temp_template = os.path.join(state["temp_dir"], "coordinates_template.xlsx")
|
||||
temp_template = os.path.join(
|
||||
state["temp_dir"], "coordinates_template.xlsx"
|
||||
)
|
||||
create_template(temp_template)
|
||||
if os.path.exists(temp_template):
|
||||
shutil.copy2(temp_template, path)
|
||||
@@ -870,7 +969,8 @@ def index():
|
||||
await save_file_with_dialog(
|
||||
"coordinates.xlsx",
|
||||
save_template,
|
||||
"Excel Files (*.xlsx)"
|
||||
"Excel Files (*.xlsx)",
|
||||
sender=e.sender,
|
||||
)
|
||||
|
||||
ui.button("导出 Excel 模板", on_click=export_template).classes(
|
||||
@@ -878,11 +978,17 @@ def index():
|
||||
).props("icon=file_download outline color=primary")
|
||||
|
||||
# 2. 上传文件区域 (垂直堆叠 Label 和 Upload 组件)
|
||||
with ui.column().classes('flex-1 gap-0 justify-between'):
|
||||
with ui.column().classes("flex-1 gap-0 justify-between"):
|
||||
# 使用 .no-list CSS 隐藏 Quasar 默认列表,完全自定义文件显示
|
||||
refs["upload_widget"] = ui.upload(
|
||||
label="选择Excel文件", on_upload=handle_upload, auto_upload=True
|
||||
).classes("w-full no-list h-full").props('flat bordered color=primary')
|
||||
refs["upload_widget"] = (
|
||||
ui.upload(
|
||||
label="选择Excel文件",
|
||||
on_upload=handle_upload,
|
||||
auto_upload=True,
|
||||
)
|
||||
.classes("w-full no-list h-full")
|
||||
.props("flat bordered color=primary")
|
||||
)
|
||||
|
||||
# 自定义文件显示容器
|
||||
refs["current_file_container"] = ui.column().classes("w-full")
|
||||
@@ -899,6 +1005,17 @@ def index():
|
||||
.classes("flex-1 py-4")
|
||||
.props("icon=play_arrow color=secondary")
|
||||
)
|
||||
# 4. 遗传算法开关
|
||||
with ui.column().classes("flex-1 gap-0 justify-center items-center"):
|
||||
refs["ga_switch"] = ui.switch("启用遗传算法", value=False).props(
|
||||
"color=orange"
|
||||
)
|
||||
|
||||
# 5. MIP开关
|
||||
with ui.column().classes("flex-1 gap-0 justify-center items-center"):
|
||||
refs["mip_switch"] = ui.switch("启用MIP", value=False).props(
|
||||
"color=blue"
|
||||
)
|
||||
|
||||
with ui.column().classes("w-full gap-4"):
|
||||
# 新增:信息展示卡片
|
||||
@@ -908,50 +1025,81 @@ def index():
|
||||
.style("max-height: 400px; overflow-y: auto;")
|
||||
):
|
||||
refs["info_container"] = ui.column().classes("w-full")
|
||||
ui.label("请上传 Excel 文件以查看系统参数和电缆规格...").classes(
|
||||
"text-gray-500 italic"
|
||||
)
|
||||
with refs["info_container"]:
|
||||
ui.label("请上传 Excel 文件以查看系统参数和电缆规格...").classes(
|
||||
"text-gray-500 italic"
|
||||
)
|
||||
|
||||
with ui.card().classes("w-full p-4 shadow-md"):
|
||||
ui.label("方案对比结果 (点击行查看拓扑详情)").classes(
|
||||
"text-xl font-semibold mb-2"
|
||||
)
|
||||
columns = [
|
||||
{
|
||||
"name": "name",
|
||||
"label": "方案名称",
|
||||
"field": "name",
|
||||
"required": True,
|
||||
"align": "left",
|
||||
},
|
||||
{
|
||||
"name": "cost_wan",
|
||||
"label": "总投资 (万元)",
|
||||
"field": "cost_wan",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "loss_kw",
|
||||
"label": "线损 (kW)",
|
||||
"field": "loss_kw",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "note",
|
||||
"label": "备注",
|
||||
"field": "note",
|
||||
"align": "left",
|
||||
},
|
||||
]
|
||||
# 使用内置的 selection='single' 结合行点击事件实现背景高亮
|
||||
# 这样可以完全由 Python 事件逻辑控制,不依赖 CSS 伪类
|
||||
refs["results_table"] = ui.table(
|
||||
columns=columns,
|
||||
rows=[],
|
||||
selection="single",
|
||||
row_key="original_name",
|
||||
).classes("w-full hide-selection-column")
|
||||
refs["results_table"].on("row-click", handle_row_click)
|
||||
with ui.card().classes("w-full p-0 shadow-md overflow-hidden"):
|
||||
with (
|
||||
ui.expansion(
|
||||
"方案对比结果 (点击行查看拓扑详情)",
|
||||
icon="analytics",
|
||||
value=True,
|
||||
)
|
||||
.classes("w-full")
|
||||
.props('header-class="text-xl font-semibold"')
|
||||
):
|
||||
columns = [
|
||||
{
|
||||
"name": "name",
|
||||
"label": "方案名称",
|
||||
"field": "name",
|
||||
"required": True,
|
||||
"align": "left",
|
||||
},
|
||||
{
|
||||
"name": "n_circuits",
|
||||
"label": "回路数",
|
||||
"field": "n_circuits",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "cost_wan",
|
||||
"label": "总投资 (万元)",
|
||||
"field": "cost_wan",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "loss_kw",
|
||||
"label": "线损 (kW)",
|
||||
"field": "loss_kw",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "total_length",
|
||||
"label": "总长度/km",
|
||||
"field": "total_length",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "npv_loss_wan",
|
||||
"label": "损耗费用净现值 (万元)",
|
||||
"field": "npv_loss_wan",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "total_cost_npv_wan",
|
||||
"label": "总费用 (万元)",
|
||||
"field": "total_cost_npv_wan",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "note",
|
||||
"label": "备注",
|
||||
"field": "note",
|
||||
"align": "left",
|
||||
},
|
||||
]
|
||||
# 使用内置的 selection='single' 结合行点击事件实现背景高亮
|
||||
# 这样可以完全由 Python 事件逻辑控制,不依赖 CSS 伪类
|
||||
refs["results_table"] = ui.table(
|
||||
columns=columns,
|
||||
rows=[],
|
||||
selection="single",
|
||||
row_key="original_name",
|
||||
).classes("w-full hide-selection-column")
|
||||
refs["results_table"].on("row-click", handle_row_click)
|
||||
with ui.card().classes("w-full p-4 shadow-md"):
|
||||
ui.label("拓扑可视化").classes("text-xl font-semibold mb-2")
|
||||
refs["plot_container"] = ui.column().classes("w-full items-center")
|
||||
@@ -1017,12 +1165,18 @@ if getattr(sys, "frozen", False):
|
||||
)
|
||||
else:
|
||||
# 普通使用环境保留日志功能
|
||||
ui.run(title="海上风电场集电线路优化", host='127.0.0.1', reload=True, port=target_port, native=False)
|
||||
# ui.run(
|
||||
# title="海上风电场集电线路优化",
|
||||
# host="127.0.0.1",
|
||||
# port=target_port,
|
||||
# reload=True,
|
||||
# window_size=(1280, 800),
|
||||
# native=True,
|
||||
# port=target_port,
|
||||
# native=False,
|
||||
# )
|
||||
ui.run(
|
||||
title="海上风电场集电线路优化",
|
||||
host="127.0.0.1",
|
||||
port=target_port,
|
||||
reload=True,
|
||||
window_size=(1280, 800),
|
||||
native=True,
|
||||
)
|
||||
|
||||
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
|
||||
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
|
||||
0
使用说明/pandoc
Normal file
0
使用说明/pandoc
Normal file
10
使用说明/使用说明.md
10
使用说明/使用说明.md
@@ -1,14 +1,16 @@
|
||||
# 海上风电场集电线路设计优化软件 - 操作手册
|
||||
|
||||
**文档版本:** v1.0
|
||||
**适用对象:** 海上能源业务开发部 - 电气专业组
|
||||
**适用对象:** 海上能源业务开发部 - 电气专业
|
||||
**编制日期:** 2026年1月5日
|
||||
|
||||
---
|
||||
|
||||
## 1. 软件概述
|
||||
|
||||
本软件专为海上风电场内集电系统(35kV/66kV/110kV)设计,旨在通过多种先进的拓扑优化算法(如Esau-Williams、MST、旋转扫描法),辅助电气工程师快速完成集电线路的路径规划与经济性比选。
|
||||
海上风电集电线路作为风电机组与海上升压站的关键连接纽带,其优化设计对提升风电场全生命周期经济性及降低环境影响具有核心意义,不仅能通过拓扑与路由优化减少建设运维成本、降低线路损耗,还能为深远海、大容量风电场开发提供技术支撑。该优化工作面临拓扑与载流量多约束耦合的技术瓶颈,需综合运用数字化技术、先进优化算法进行应对。
|
||||
|
||||
本软件专为海上风电场内集电系统(35kV/66kV/110kV)设计,旨在通过多种先进的拓扑优化算法(如Esau-Williams、MST、旋转扫描法),辅助电气专业快速完成集电线路的路径规划与经济性比选。
|
||||
|
||||
软件能够根据风机坐标、海缆载流量及造价数据,自动计算并生成线损最小、投资最优的接线方案,并支持一键导出 CAD 图纸和海缆长度。
|
||||
|
||||
@@ -141,7 +143,7 @@
|
||||
### 5.2 导出 Excel 报告
|
||||
点击 **“下载 Excel 对比表”**,将生成一份包含详细工程数据的 Excel 文件,内容包括:
|
||||
* **Summary**: 所有方案的经济技术指标汇总。
|
||||
* **Details**: 推荐方案的每一条海缆连接明细(起点、终点、长度、型号、负载率)。
|
||||
* **Details**: 推荐方案的每一条海缆连接明细(起点、终点、长度、型号)。
|
||||
|
||||
### 5.3 批量归档
|
||||
点击 **“导出全部方案 DXF (ZIP)”**,可将所有计算产生的方案图纸和报表打包下载,便于项目归档。
|
||||
@@ -160,4 +162,4 @@ A: 软件算法以不超过额定载流量为约束条件(默认允许 100%
|
||||
A: 请双击鼠标滚轮(Zoom Extents)全屏显示。由于风机坐标通常是大地坐标(数值很大),如果 CAD 当前视口在 (0,0) 附近,可能会找不到图形。
|
||||
|
||||
---
|
||||
**技术支持:** 海上能源业务开发部 - 数字化小组
|
||||
**技术支持:** 海上能源业务开发部 - 杜孟远
|
||||
|
||||
BIN
使用说明/使用说明.pdf
Normal file
BIN
使用说明/使用说明.pdf
Normal file
Binary file not shown.
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