From a5b46529dad9986aab8757c25f9a7854798e0936 Mon Sep 17 00:00:00 2001 From: dmy Date: Mon, 5 Jan 2026 09:52:51 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=93=E5=8C=85?= =?UTF-8?q?=E5=90=8Eexe=E7=A8=8B=E5=BA=8F=E5=9C=A8=E6=97=A0=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=8F=B0=E6=A8=A1=E5=BC=8F=E4=B8=8B=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=B6=E7=9A=84uvicorn=E6=97=A5=E5=BF=97=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通过检测sys.frozen判断运行环境,只在打包后的exe程序中禁用日志配置, 避免AttributeError: 'NoneType' object has no attribute 'isatty'错误。 普通Python运行环境保留完整日志功能,方便调试。 --- README.md | 142 ++++++++++++++++--------------------------- generate_template.py | 12 +++- gui.py | 96 ++++++++++++++++++++++++++++- main.py | 77 +++++++++++++++++------ 4 files changed, 214 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index e6b6697..821c74a 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,78 @@ -# 海上风电场集电系统设计工具 +# 海上风电场集电系统设计优化工具 (Wind Farm Collector System Optimizer) -一个用于设计和优化海上风电场集电系统的Python工具,支持多种布局算法和电缆优化方案。 +一个用于设计和优化海上风电场集电系统拓扑的综合工具。支持多种先进算法,能够根据风机坐标、功率以及海缆规格,自动生成投资成本最低、损耗最小的设计方案。 -## 功能特性 +## 🌟 主要功能 -- 🌊 多种风机布局生成(随机分布、规则网格) -- 🔌 多种集电系统设计算法: - - 最小生成树(MST)算法 - - K-means聚类算法 - - 容量扫描算法(Capacitated Sweep) - - 旋转优化算法(Rotational Sweep) -- 📊 多方案对比分析和可视化 -- 📋 自动导出DXF图纸和Excel报告 -- 🔧 智能电缆规格选择和成本优化 +- 🖥️ **交互式 Web 界面**:基于 NiceGUI 开发,支持文件上传、实时日志、方案对比和可视化。 +- 🌊 **多种布局生成**:内置模拟数据生成器,支持规则网格和随机分布布局。 +- 🔌 **先进设计算法**: + - **MST (Minimum Spanning Tree)**:无容量约束基准方案。 + - **Capacitated Sweep (Base)**:基础扇区扫描分组。 + - **Rotational Sweep**:全局最优起始角度旋转扫描优化。 + - **Esau-Williams**:经典启发式算法,在距离与容量间寻找最优平衡。 +- 📊 **智能方案对比**:自动运行三大场景(标准方案、含可选电缆方案、限制最大截面方案)并对比结果。 +- 📋 **数据校验与保障**:严格校验输入数据的有序性及电缆配置规则。 +- 📁 **多格式导出**: + - 自动生成 CAD 图纸 (`.dxf`),按电缆规格分层并着色。 + - 导出详细的 Excel 对比报告及单方案电缆清册。 + - 支持一键打包导出所有方案压缩包 (`.zip`)。 -## 安装依赖 +## 🛠️ 安装依赖 + +本项目使用 `uv` 或 `pip` 管理环境。推荐安装依赖: ```bash -pip install numpy pandas matplotlib scikit-learn scipy networkx +pip install numpy pandas matplotlib scikit-learn scipy networkx ezdxf nicegui openpyxl ``` -## 使用方法 +## 🚀 使用方法 -### 基本用法 +### 1. 启动图形化界面 (推荐) + +运行以下命令启动 Web 界面,程序将自动在浏览器中打开: ```bash -python main.py +python gui.py ``` +*注:程序默认监听 8080 端口,若被占用将自动尝试后续可用端口。* -### 指定数据文件 +### 2. 命令行模式 ```bash -python main.py --excel wind_farm_coordinates.xlsx +python main.py --excel your_data.xlsx ``` -### 覆盖默认簇数 +## 📝 输入数据规范 (Excel) -```bash -python main.py --clusters 20 -``` +为了确保计算结果的准确性,输入 Excel 文件应包含以下两个 Sheet: -## 算法说明 +### 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 -- 将风机分组到多个回路中 -- 平衡每回路的功率分配 +## 📈 场景说明 (Scenarios) -### 3. Capacitated Sweep(容量扫描) -- 考虑电缆容量约束的智能分组 -- 支持多种电缆规格自动选择 +1. **Scenario 1 (Standard)**:仅使用非可选(标准)电缆进行优化。 +2. **Scenario 2 (With Optional)**:包含标记为 'Y' 的大型电缆,适用于尝试增加单回路容量的场景。 +3. **Scenario 3 (No Max)**:排除最大截面电缆,测试在电缆供应受限时的最优拓扑。 -### 4. Rotational Sweep(旋转优化) -- 在容量扫描基础上进行旋转优化 -- 进一步降低总成本和损耗 +## 📂 输出文件说明 -## 输出文件 +- **Excel 报告**:`[文件名]_result.xlsx` 包含所有方案的总览及详细连接清单。 +- **CAD 图纸**:`design_[方案名].dxf` 包含分层分色的拓扑图。 +- **全部方案**:`[文件名]_result.zip` 包含所有图纸及 Excel 报告。 -1. **可视化图片**:`wind_farm_design_comparison.png` - - 不同算法的设计方案对比图 +## ⚖️ 许可证 -2. **CAD图纸**:`wind_farm_design.dxf` - - 可导入CAD软件的详细设计图纸 - -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) (默认) -``` - -## 许可证 - -本项目仅供学习和研究使用。 \ No newline at end of file +本项目仅供工程学习、研究和初步设计评估使用。详细计算应以专业设计院规范为准。 \ No newline at end of file diff --git a/generate_template.py b/generate_template.py index e95c135..337c387 100644 --- a/generate_template.py +++ b/generate_template.py @@ -50,12 +50,20 @@ def create_template(): ] df_cables = pd.DataFrame(cable_data) + # Create System Parameters data + param_data = [ + {'Parameter': 'Voltage (kV) / 电压 (kV)', 'Value': 66}, + {'Parameter': 'Power Factor / 功率因数', 'Value': 0.95} + ] + df_params = pd.DataFrame(param_data) + # Save to Excel - output_file = 'coordinates.xlsx' + output_file = 'windfarm_template.xlsx' with pd.ExcelWriter(output_file) as writer: df.to_excel(writer, sheet_name='Coordinates', index=False) df_cables.to_excel(writer, sheet_name='Cables', index=False) - 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__": create_template() \ No newline at end of file diff --git a/gui.py b/gui.py index df25e39..9879f82 100644 --- a/gui.py +++ b/gui.py @@ -38,6 +38,8 @@ state = { "results": [], "substation": None, "turbines": None, + "cable_specs": None, + "system_params": None, "temp_dir": os.path.join(tempfile.gettempdir(), "windfarm_gui_uploads"), } @@ -70,8 +72,74 @@ def index(): "upload_widget": None, "run_btn": None, "current_file_label": None, + "info_container": None, # 新增信息展示容器 } + def update_info_panel(): + if refs["info_container"]: + refs["info_container"].clear() + with refs["info_container"]: + # System Params - Always show + with ui.row().classes("w-full items-center gap-4 mb-2"): + ui.icon('settings', color='primary').classes('text-2xl') + ui.label("系统参数").classes("text-lg font-bold") + + params_text = [] + + # 获取电压 + v = 66000 # Default + is_default_v = True + if state.get("system_params") and 'voltage' in state['system_params']: + v = state['system_params']['voltage'] + is_default_v = False + + 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) + + # 获取功率因数 + pf = 0.95 # Default + is_default_pf = True + if state.get("system_params") and 'power_factor' in state['system_params']: + pf = state['system_params']['power_factor'] + is_default_pf = False + + pf_str = f"功率因数: {pf}" + if is_default_pf: + pf_str += " (默认)" + params_text.append(pf_str) + + for p in params_text: + ui.chip(p, icon='bolt').props('outline color=primary') + + ui.separator().classes('my-2') + + # Cables + if state.get("cable_specs"): + with ui.row().classes("w-full items-center gap-2 mb-2"): + ui.icon('cable', color='secondary').classes('text-2xl') + ui.label("电缆规格参数").classes("text-lg font-bold") + + columns = [ + {'name': 'section', 'label': '截面 (mm²)', 'field': 'section', 'align': 'center'}, + {'name': 'capacity', 'label': '载流量 (A)', 'field': 'capacity', 'align': 'center'}, + {'name': 'resistance', 'label': '电阻 (Ω/km)', 'field': 'resistance', 'align': 'center'}, + {'name': 'cost', 'label': '参考单价 (元/m)', 'field': 'cost', 'align': 'center'}, + ] + rows = [] + for spec in state["cable_specs"]: + # spec is (section, capacity, resistance, cost, is_optional) + rows.append({ + 'section': spec[0], + 'capacity': spec[1], + 'resistance': spec[2], + 'cost': spec[3] + }) + ui.table(columns=columns, rows=rows).classes('w-full').props('dense flat bordered') + else: + ui.label("未检测到电缆数据,将使用默认参数。").classes("text-gray-500 italic") + async def handle_upload(e: events.UploadEventArguments): try: filename = None @@ -125,7 +193,16 @@ def index(): refs["current_file_label"].text = f"当前文件: {filename}" # 加载数据 - state["turbines"], state["substation"], _ = load_data_from_excel(path) + try: + # 尝试解包 4 个返回值 (新版 main.py) + state["turbines"], state["substation"], state["cable_specs"], state["system_params"] = load_data_from_excel(path) + except ValueError: + # 兼容旧版 (如果是 3 个返回值) + state["turbines"], state["substation"], state["cable_specs"] = load_data_from_excel(path) + state["system_params"] = {} + + update_info_panel() + except Exception as ex: ui.notify(f"上传处理失败: {ex}", type="negative") @@ -546,6 +623,11 @@ def index(): ) with ui.column().classes("w-3/4 gap-4"): + # 新增:信息展示卡片 + with ui.card().classes("w-full p-4 shadow-md").style("max-height: 400px; overflow-y: auto;"): + refs["info_container"] = ui.column().classes("w-full") + 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" @@ -610,4 +692,14 @@ def find_available_port(start_port=8080, max_attempts=10): # 自动寻找可用端口,避免端口冲突 target_port = find_available_port(8080) -ui.run(title="海上风电场集电线路优化", port=target_port) + +# 检测是否为打包后的exe程序 +import sys +if getattr(sys, 'frozen', False): + # 打包环境下禁用日志配置,避免在无控制台模式下出现错误 + import logging + logging.disable(logging.CRITICAL) + ui.run(title="海上风电场集电线路优化", port=target_port, log_level=None) +else: + # 普通使用环境保留日志功能 + ui.run(title="海上风电场集电线路优化", port=target_port) diff --git a/main.py b/main.py index 6e51931..9cbd15c 100644 --- a/main.py +++ b/main.py @@ -199,7 +199,39 @@ def load_data_from_excel(file_path): if cable_specs: print(f"成功加载: {len(cable_specs)} 种电缆规格") - return turbines, substation, cable_specs + # 读取参数数据 (如果存在) + system_params = {} + param_sheet_name = None + if 'Parameters' in xl.sheet_names: + param_sheet_name = 'Parameters' + elif '参数' in xl.sheet_names: + param_sheet_name = '参数' + + if param_sheet_name: + try: + params_df = pd.read_excel(xl, param_sheet_name) + # 假设格式为两列:Parameter (参数名), Value (值) + if len(params_df.columns) >= 2: + for _, row in params_df.iterrows(): + key = str(row[0]).strip().lower() + try: + val = float(row[1]) + if 'voltage' in key or '电压' in key: + # 检测是否为kV单位 + if 'kv' in key: + system_params['voltage'] = val * 1000 + else: + system_params['voltage'] = val + elif 'factor' in key or '功率因数' in key: + system_params['power_factor'] = val + except ValueError: + pass + if system_params: + print(f"成功加载系统参数: {system_params}") + except Exception as e: + print(f"读取参数Sheet失败: {e}") + + return turbines, substation, cable_specs, system_params except Exception as e: print(f"读取Excel文件失败: {str(e)}") @@ -315,7 +347,7 @@ def design_with_kmeans(turbines, substation, n_clusters=3): return cluster_connections + substation_connections, turbines # 3.5 带容量约束的扇区扫描算法 (Capacitated Sweep) - 基础版 -def design_with_capacitated_sweep(turbines, substation, cable_specs=None): +def design_with_capacitated_sweep(turbines, substation, cable_specs=None, voltage=VOLTAGE_LEVEL, power_factor=POWER_FACTOR): """ 使用带容量约束的扇区扫描算法设计集电线路 (基础版:单次扫描) 原理: @@ -325,10 +357,9 @@ def design_with_capacitated_sweep(turbines, substation, cable_specs=None): 4. 满载后开启新回路。 """ # 1. 获取电缆最大容量 - max_mw = get_max_cable_capacity_mw(cable_specs) - - substation_coord = substation[0] + max_mw = get_max_cable_capacity_mw(cable_specs, voltage=voltage, power_factor=power_factor) + substation_coord = substation[0] # 2. 计算角度 (使用 arctan2 返回 -pi 到 pi) work_df = turbines.copy() dx = work_df['x'] - substation_coord[0] @@ -417,7 +448,7 @@ def design_with_capacitated_sweep(turbines, substation, cable_specs=None): return cluster_connections + substation_connections, turbines # 3.6 旋转扫描算法 (Rotational Sweep) - 优化版 -def design_with_rotational_sweep(turbines, substation, cable_specs=None): +def design_with_rotational_sweep(turbines, substation, cable_specs=None, voltage=VOLTAGE_LEVEL, power_factor=POWER_FACTOR): """ 使用带容量约束的扇区扫描算法设计集电线路 (优化版:旋转扫描) 原理: @@ -427,7 +458,7 @@ def design_with_rotational_sweep(turbines, substation, cable_specs=None): 4. 对每种分组方案计算MST成本,选出总成本最低的方案。 """ # 1. 获取电缆最大容量 - max_mw = get_max_cable_capacity_mw(cable_specs) + max_mw = get_max_cable_capacity_mw(cable_specs, voltage=voltage, power_factor=power_factor) substation_coord = substation[0] @@ -541,7 +572,7 @@ def design_with_rotational_sweep(turbines, substation, cable_specs=None): return final_connections, turbines -def get_max_cable_capacity_mw(cable_specs=None): +def get_max_cable_capacity_mw(cable_specs=None, voltage=VOLTAGE_LEVEL, power_factor=POWER_FACTOR): """ 计算给定电缆规格中能够承载的最大功率 (单位: MW)。 @@ -549,6 +580,8 @@ def get_max_cable_capacity_mw(cable_specs=None): 参数: cable_specs (list, optional): 电缆规格列表。每个元素应包含 (截面积, 额定电流, 单价, 损耗系数)。 + voltage (float): 系统电压 (V), 默认 66000 + power_factor (float): 功率因数, 默认 0.95 返回: float: 最大功率承载能力 (MW)。 @@ -576,13 +609,13 @@ def get_max_cable_capacity_mw(cable_specs=None): # 计算最大功率:P = √3 * U * I * cosφ # 这里假设降额系数为 1 (不降额) max_current = max_current_capacity * 1 - max_power_w = np.sqrt(3) * VOLTAGE_LEVEL * max_current * POWER_FACTOR + max_power_w = np.sqrt(3) * voltage * max_current * power_factor # 将单位从 W 转换为 MW return max_power_w / 1e6 # 5. 计算集电线路方案成本 -def evaluate_design(turbines, connections, substation, cable_specs=None, is_offshore=False, method_name="Unknown Method"): +def evaluate_design(turbines, connections, substation, cable_specs=None, is_offshore=False, method_name="Unknown Method", voltage=VOLTAGE_LEVEL, power_factor=POWER_FACTOR): """评估设计方案的总成本和损耗""" total_cost = 0 total_loss = 0 @@ -685,7 +718,7 @@ def evaluate_design(turbines, connections, substation, cable_specs=None, is_offs cable_specs_to_use = cable_specs # 估算电流 - current = (power * 1e6) / (np.sqrt(3) * VOLTAGE_LEVEL * POWER_FACTOR) + current = (power * 1e6) / (np.sqrt(3) * voltage * power_factor) # 选择满足载流量的最小电缆 selected_spec = None @@ -1055,10 +1088,12 @@ def compare_design_methods(excel_path=None, n_clusters_override=None, interactiv :param plot_results: 是否生成和保存对比图表 """ cable_specs = None + system_params = {} + if excel_path: print(f"正在从 {excel_path} 读取坐标数据...") try: - turbines, substation, cable_specs = load_data_from_excel(excel_path) + turbines, substation, cable_specs, system_params = load_data_from_excel(excel_path) scenario_title = "Offshore Wind Farm (Imported Data)" except Exception: print("回退到自动生成数据模式...") @@ -1070,6 +1105,10 @@ def compare_design_methods(excel_path=None, n_clusters_override=None, interactiv is_offshore = True + voltage = system_params.get('voltage', VOLTAGE_LEVEL) + power_factor = system_params.get('power_factor', POWER_FACTOR) + print(f"使用的系统参数: 电压={voltage} V, 功率因数={power_factor}") + # 准备三种电缆方案 # 原始 specs 是 5 元素元组: (section, capacity, resistance, cost, is_optional) # 下游函数期望 4 元素元组: (section, capacity, resistance, cost) @@ -1114,7 +1153,7 @@ def compare_design_methods(excel_path=None, n_clusters_override=None, interactiv # 1. MST 方法作为基准 (使用 Scenario 1) mst_connections = design_with_mst(turbines, substation) - mst_evaluation = evaluate_design(turbines, mst_connections, substation, cable_specs=specs_1, is_offshore=is_offshore, method_name="MST Method") + mst_evaluation = evaluate_design(turbines, mst_connections, substation, cable_specs=specs_1, is_offshore=is_offshore, method_name="MST Method", voltage=voltage, power_factor=power_factor) # 准备画布 2x2 fig = None @@ -1152,7 +1191,7 @@ def compare_design_methods(excel_path=None, n_clusters_override=None, interactiv # 计算参数 total_power = turbines['power'].sum() - max_cable_mw = get_max_cable_capacity_mw(cable_specs=current_specs) + max_cable_mw = get_max_cable_capacity_mw(cable_specs=current_specs, voltage=voltage, power_factor=power_factor) # 确定簇数 (针对 Base 算法) if n_clusters_override is not None: @@ -1172,11 +1211,11 @@ def compare_design_methods(excel_path=None, n_clusters_override=None, interactiv # --- Run 1: Base Algorithm (Capacitated Sweep) --- base_name = f"{name} (Base)" conns_base, turbines_base = design_with_capacitated_sweep( - turbines.copy(), substation, cable_specs=current_specs + turbines.copy(), substation, cable_specs=current_specs, voltage=voltage, power_factor=power_factor ) eval_base = evaluate_design( turbines, conns_base, substation, cable_specs=current_specs, - is_offshore=is_offshore, method_name=base_name + is_offshore=is_offshore, method_name=base_name, voltage=voltage, power_factor=power_factor ) comparison_results.append({ @@ -1192,11 +1231,11 @@ def compare_design_methods(excel_path=None, n_clusters_override=None, interactiv # --- Run 2: Rotational Algorithm (Optimization) --- rot_name = f"{name} (Rotational)" conns_rot, turbines_rot = design_with_rotational_sweep( - turbines.copy(), substation, cable_specs=current_specs + turbines.copy(), substation, cable_specs=current_specs, voltage=voltage, power_factor=power_factor ) eval_rot = evaluate_design( turbines, conns_rot, substation, cable_specs=current_specs, - is_offshore=is_offshore, method_name=rot_name + is_offshore=is_offshore, method_name=rot_name, voltage=voltage, power_factor=power_factor ) comparison_results.append({ @@ -1216,7 +1255,7 @@ def compare_design_methods(excel_path=None, n_clusters_override=None, interactiv ) eval_ew = evaluate_design( turbines, conns_ew, substation, cable_specs=current_specs, - is_offshore=is_offshore, method_name=ew_name + is_offshore=is_offshore, method_name=ew_name, voltage=voltage, power_factor=power_factor ) comparison_results.append({