feat: 新增Web GUI界面,支持交互式设计对比
This commit is contained in:
238
gui.py
Normal file
238
gui.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
import contextlib
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from nicegui import ui, events
|
||||||
|
from main import compare_design_methods, export_to_dxf, load_data_from_excel, generate_wind_farm_data, visualize_design
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# 设置matplotlib支持中文显示
|
||||||
|
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial']
|
||||||
|
plt.rcParams['axes.unicode_minus'] = False
|
||||||
|
|
||||||
|
class Logger(io.StringIO):
|
||||||
|
def __init__(self, log_element):
|
||||||
|
super().__init__()
|
||||||
|
self.log_element = log_element
|
||||||
|
|
||||||
|
def write(self, message):
|
||||||
|
if message.strip():
|
||||||
|
self.log_element.push(message.strip())
|
||||||
|
super().write(message)
|
||||||
|
|
||||||
|
# 状态变量
|
||||||
|
state = {
|
||||||
|
'excel_path': None,
|
||||||
|
'results': [],
|
||||||
|
'substation': None,
|
||||||
|
'turbines': None,
|
||||||
|
'temp_dir': '.gemini/tmp/gui_uploads'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 确保临时目录存在
|
||||||
|
if not os.path.exists(state['temp_dir']):
|
||||||
|
os.makedirs(state['temp_dir'], exist_ok=True)
|
||||||
|
|
||||||
|
@ui.page('/')
|
||||||
|
def index():
|
||||||
|
ui.query('body').style('background-color: #f0f2f5; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;')
|
||||||
|
|
||||||
|
# 定义 UI 元素引用容器,方便在函数中更新
|
||||||
|
refs = {
|
||||||
|
'log_box': None,
|
||||||
|
'results_table': None,
|
||||||
|
'plot_container': None,
|
||||||
|
'export_row': None,
|
||||||
|
'status_label': None,
|
||||||
|
'upload_widget': None,
|
||||||
|
'clusters_input': None,
|
||||||
|
'run_btn': None
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_upload(e: events.UploadEventArguments):
|
||||||
|
try:
|
||||||
|
filename = None
|
||||||
|
content = None
|
||||||
|
if hasattr(e, 'name'):
|
||||||
|
filename = e.name
|
||||||
|
if hasattr(e, 'content'):
|
||||||
|
content = e.content
|
||||||
|
if content is None and hasattr(e, 'file'):
|
||||||
|
file_obj = e.file
|
||||||
|
if not filename:
|
||||||
|
filename = getattr(file_obj, 'name', getattr(file_obj, 'filename', None))
|
||||||
|
if hasattr(file_obj, 'file') and hasattr(file_obj.file, 'read'):
|
||||||
|
content = file_obj.file
|
||||||
|
elif hasattr(file_obj, 'read'):
|
||||||
|
content = file_obj
|
||||||
|
if not filename:
|
||||||
|
filename = 'uploaded_file.xlsx'
|
||||||
|
if content is None:
|
||||||
|
ui.notify('上传失败: 无法解析文件内容', type='negative')
|
||||||
|
return
|
||||||
|
path = os.path.join(state['temp_dir'], filename)
|
||||||
|
if hasattr(content, 'seek'):
|
||||||
|
try: content.seek(0)
|
||||||
|
except Exception: pass
|
||||||
|
data = content.read()
|
||||||
|
import inspect
|
||||||
|
if inspect.iscoroutine(data):
|
||||||
|
data = await data
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
state['excel_path'] = path
|
||||||
|
ui.notify(f'文件已上传: {filename}', type='positive')
|
||||||
|
state['turbines'], state['substation'], _ = load_data_from_excel(path)
|
||||||
|
except Exception as ex:
|
||||||
|
ui.notify(f'上传处理失败: {ex}', type='negative')
|
||||||
|
|
||||||
|
def update_export_buttons():
|
||||||
|
if refs['export_row']:
|
||||||
|
refs['export_row'].clear()
|
||||||
|
if not state['results'] or not refs['export_row']:
|
||||||
|
return
|
||||||
|
with refs['export_row']:
|
||||||
|
ui.button('下载 Excel 对比表', on_click=lambda: ui.download('wind_farm_design.xlsx')).props('icon=download')
|
||||||
|
best_idx = 0
|
||||||
|
for i, res in enumerate(state['results']):
|
||||||
|
if res['cost'] < state['results'][best_idx]['cost']:
|
||||||
|
best_idx = i
|
||||||
|
best_res = state['results'][best_idx]
|
||||||
|
def export_best_dxf():
|
||||||
|
dxf_name = 'best_design.dxf'
|
||||||
|
if state['substation'] is not None:
|
||||||
|
export_to_dxf(best_res['turbines'], state['substation'], best_res['eval']['details'], dxf_name)
|
||||||
|
ui.download(dxf_name)
|
||||||
|
ui.notify(f'已导出推荐方案: {best_res["name"]}', type='positive')
|
||||||
|
else:
|
||||||
|
ui.notify('缺少升压站数据,无法导出 DXF', type='negative')
|
||||||
|
ui.button(f'导出推荐方案 DXF ({best_res["name"]})', on_click=export_best_dxf).props('icon=architecture color=accent')
|
||||||
|
|
||||||
|
def update_plot(result):
|
||||||
|
if refs['plot_container']:
|
||||||
|
refs['plot_container'].clear()
|
||||||
|
with refs['plot_container']:
|
||||||
|
with ui.pyplot(figsize=(10, 8)) as plot:
|
||||||
|
title = f"{result['name']}\nCost: ¥{result['cost']/10000:.2f}万 | Loss: {result['loss']:.2f} kW"
|
||||||
|
# 获取当前 ui.pyplot 创建的 axes
|
||||||
|
ax = plt.gca()
|
||||||
|
visualize_design(result['turbines'], state['substation'], result['eval']['details'], title, ax=ax)
|
||||||
|
|
||||||
|
def handle_row_click(e):
|
||||||
|
if not e.args or 'data' not in e.args: return
|
||||||
|
row_name = e.args['data']['name']
|
||||||
|
selected_res = next((r for r in state['results'] if r['name'] == row_name), None)
|
||||||
|
if selected_res:
|
||||||
|
update_plot(selected_res)
|
||||||
|
ui.notify(f'已切换至方案: {row_name}')
|
||||||
|
|
||||||
|
from nicegui import run
|
||||||
|
import queue
|
||||||
|
|
||||||
|
async def run_analysis(n_clusters):
|
||||||
|
if not state['excel_path']:
|
||||||
|
ui.notify('请先上传 Excel 坐标文件!', type='warning')
|
||||||
|
if refs['log_box']:
|
||||||
|
refs['log_box'].clear()
|
||||||
|
log_queue = queue.Queue()
|
||||||
|
class QueueLogger(io.StringIO):
|
||||||
|
def write(self, message):
|
||||||
|
if message and message.strip():
|
||||||
|
log_queue.put(message.strip())
|
||||||
|
super().write(message)
|
||||||
|
def process_log_queue():
|
||||||
|
if refs['log_box']:
|
||||||
|
while not log_queue.empty():
|
||||||
|
try:
|
||||||
|
msg = log_queue.get_nowait()
|
||||||
|
refs['log_box'].push(msg)
|
||||||
|
if msg.startswith('--- Scenario'):
|
||||||
|
scenario_name = msg.replace('---', '').strip()
|
||||||
|
if refs['status_label']:
|
||||||
|
refs['status_label'].text = f"正在计算: {scenario_name}..."
|
||||||
|
elif '开始比较电缆方案' in msg:
|
||||||
|
if refs['status_label']: refs['status_label'].text = "准备开始计算..."
|
||||||
|
except queue.Empty: break
|
||||||
|
log_timer = ui.timer(0.1, process_log_queue)
|
||||||
|
if refs['status_label']: refs['status_label'].text = "初始化中..."
|
||||||
|
processing_dialog.open()
|
||||||
|
try:
|
||||||
|
# 2. 定义在线程中运行的任务
|
||||||
|
def task():
|
||||||
|
# 捕获 stdout 到我们的 QueueLogger
|
||||||
|
with contextlib.redirect_stdout(QueueLogger()):
|
||||||
|
return compare_design_methods(
|
||||||
|
excel_path=state['excel_path'],
|
||||||
|
n_clusters_override=n_clusters,
|
||||||
|
interactive=False,
|
||||||
|
plot_results=False # 禁止后台绘图,避免线程安全问题
|
||||||
|
)
|
||||||
|
results = await run.io_bound(task)
|
||||||
|
state['results'] = results
|
||||||
|
if not state['excel_path'] and results:
|
||||||
|
if state['substation'] is None:
|
||||||
|
_, state['substation'] = generate_wind_farm_data(n_turbines=30, layout='grid', spacing=800)
|
||||||
|
if refs['results_table']:
|
||||||
|
table_data = []
|
||||||
|
for res in results:
|
||||||
|
table_data.append({'name': res['name'], 'cost_wan': round(res['cost'] / 10000, 2), 'loss_kw': round(res['loss'], 2)})
|
||||||
|
refs['results_table'].rows = table_data
|
||||||
|
refs['results_table'].update()
|
||||||
|
|
||||||
|
# 计算完成后,自动寻找并显示最佳方案的拓扑图 (不再显示4合1大图)
|
||||||
|
if results:
|
||||||
|
best_res = min(results, key=lambda x: x['cost'])
|
||||||
|
update_plot(best_res)
|
||||||
|
ui.notify(f'计算完成!已自动加载推荐方案: {best_res["name"]}', type='positive')
|
||||||
|
|
||||||
|
update_export_buttons()
|
||||||
|
if refs['status_label']: refs['status_label'].text = "计算完成!"
|
||||||
|
except Exception as ex:
|
||||||
|
ui.notify(f'运行出错: {ex}', type='negative')
|
||||||
|
finally:
|
||||||
|
log_timer.cancel()
|
||||||
|
process_log_queue()
|
||||||
|
processing_dialog.close()
|
||||||
|
|
||||||
|
with ui.dialog() as processing_dialog:
|
||||||
|
with ui.card().classes('w-96 items-center justify-center p-6'):
|
||||||
|
ui.label('正在计算方案...').classes('text-xl font-bold text-primary mb-2')
|
||||||
|
ui.spinner(size='lg', color='primary')
|
||||||
|
refs['status_label'] = ui.label('准备中...').classes('mt-4 text-sm text-gray-700 font-medium')
|
||||||
|
with ui.expansion('查看实时日志', icon='terminal', value=True).classes('w-full mt-4 text-sm'):
|
||||||
|
refs['log_box'] = ui.log(max_lines=100).classes('w-full h-32 text-xs font-mono bg-black text-green-400')
|
||||||
|
processing_dialog.props('persistent')
|
||||||
|
|
||||||
|
with ui.header().classes('bg-primary text-white p-4 shadow-lg'):
|
||||||
|
ui.label('海上风电场集电线路设计优化系统').classes('text-2xl font-bold')
|
||||||
|
ui.label('Wind Farm Collector System Design Optimizer').classes('text-sm opacity-80')
|
||||||
|
|
||||||
|
with ui.row().classes('w-full p-4 gap-4'):
|
||||||
|
with ui.card().classes('w-1/4 p-4 shadow-md'):
|
||||||
|
ui.label('配置面板').classes('text-xl font-semibold mb-4 border-b pb-2')
|
||||||
|
ui.label('1. 上传坐标文件 (.xlsx)').classes('font-medium')
|
||||||
|
refs['upload_widget'] = ui.upload(label='选择Excel文件', on_upload=handle_upload, auto_upload=True).classes('w-full mb-4')
|
||||||
|
ui.label('2. 参数设置').classes('font-medium mt-4')
|
||||||
|
refs['clusters_input'] = ui.number('指定回路数 (可选)', value=None, format='%d', placeholder='自动计算').classes('w-full mb-4')
|
||||||
|
refs['run_btn'] = ui.button('运行方案对比', on_click=lambda: run_analysis(refs['clusters_input'].value)).classes('w-full mt-4 py-4').props('icon=play_arrow color=secondary')
|
||||||
|
|
||||||
|
with ui.column().classes('w-3/4 gap-4'):
|
||||||
|
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},
|
||||||
|
]
|
||||||
|
# 移除 selection='single',改为纯行点击交互
|
||||||
|
refs['results_table'] = ui.table(columns=columns, rows=[]).classes('w-full')
|
||||||
|
refs['results_table'].on('rowClick', 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')
|
||||||
|
with ui.card().classes('w-full p-4 shadow-md'):
|
||||||
|
ui.label('导出与下载').classes('text-xl font-semibold mb-2')
|
||||||
|
refs['export_row'] = ui.row().classes('gap-4')
|
||||||
|
|
||||||
|
ui.run(title='海上风电场集电线路优化', port=8080)
|
||||||
94
main.py
94
main.py
@@ -287,20 +287,17 @@ def design_with_capacitated_sweep(turbines, substation, cable_specs=None):
|
|||||||
"""
|
"""
|
||||||
# 1. 获取电缆最大容量
|
# 1. 获取电缆最大容量
|
||||||
max_mw = get_max_cable_capacity_mw(cable_specs)
|
max_mw = get_max_cable_capacity_mw(cable_specs)
|
||||||
# print(f"DEBUG: 扇区扫描算法启动 - 单回路容量限制: {max_mw:.2f} MW")
|
|
||||||
|
|
||||||
substation_coord = substation[0]
|
substation_coord = substation[0]
|
||||||
|
|
||||||
# 2. 计算角度 (使用 arctan2 返回 -pi 到 pi)
|
# 2. 计算角度 (使用 arctan2 返回 -pi 到 pi)
|
||||||
# 避免直接修改原始DataFrame,使用副本
|
|
||||||
work_df = turbines.copy()
|
work_df = turbines.copy()
|
||||||
dx = work_df['x'] - substation_coord[0]
|
dx = work_df['x'] - substation_coord[0]
|
||||||
dy = work_df['y'] - substation_coord[1]
|
dy = work_df['y'] - substation_coord[1]
|
||||||
work_df['angle'] = np.arctan2(dy, dx)
|
work_df['angle'] = np.arctan2(dy, dx)
|
||||||
|
|
||||||
# 3. 寻找最佳起始角度 (最大角度间隙)
|
# 3. 寻找最佳起始角度 (最大角度间隙)
|
||||||
# 按角度排序
|
work_df = work_df.sort_values('angle').reset_index(drop=True)
|
||||||
work_df = work_df.sort_values('angle').reset_index(drop=True) # 重置索引方便切片
|
|
||||||
|
|
||||||
angles = work_df['angle'].values
|
angles = work_df['angle'].values
|
||||||
n = len(angles)
|
n = len(angles)
|
||||||
@@ -392,7 +389,6 @@ def design_with_rotational_sweep(turbines, substation, cable_specs=None):
|
|||||||
"""
|
"""
|
||||||
# 1. 获取电缆最大容量
|
# 1. 获取电缆最大容量
|
||||||
max_mw = get_max_cable_capacity_mw(cable_specs)
|
max_mw = get_max_cable_capacity_mw(cable_specs)
|
||||||
# print(f"DEBUG: 扇区扫描算法启动 - 单回路容量限制: {max_mw:.2f} MW")
|
|
||||||
|
|
||||||
substation_coord = substation[0]
|
substation_coord = substation[0]
|
||||||
|
|
||||||
@@ -410,6 +406,7 @@ def design_with_rotational_sweep(turbines, substation, cable_specs=None):
|
|||||||
best_connections = []
|
best_connections = []
|
||||||
best_turbines_state = None
|
best_turbines_state = None
|
||||||
best_start_idx = -1
|
best_start_idx = -1
|
||||||
|
best_id_to_cluster = {}
|
||||||
|
|
||||||
# 遍历所有可能的起始点
|
# 遍历所有可能的起始点
|
||||||
for start_idx in range(n_turbines):
|
for start_idx in range(n_turbines):
|
||||||
@@ -463,8 +460,7 @@ def design_with_rotational_sweep(turbines, substation, cable_specs=None):
|
|||||||
# 2. 连接升压站长度
|
# 2. 连接升压站长度
|
||||||
dists = np.sqrt((cluster_rows['x'] - substation_coord[0])**2 +
|
dists = np.sqrt((cluster_rows['x'] - substation_coord[0])**2 +
|
||||||
(cluster_rows['y'] - substation_coord[1])**2)
|
(cluster_rows['y'] - substation_coord[1])**2)
|
||||||
min_dist = dists.min()
|
current_total_length += dists.min()
|
||||||
current_total_length += min_dist
|
|
||||||
|
|
||||||
# --- 比较并保存最佳结果 ---
|
# --- 比较并保存最佳结果 ---
|
||||||
if current_total_length < best_cost:
|
if current_total_length < best_cost:
|
||||||
@@ -582,8 +578,8 @@ def evaluate_design(turbines, connections, substation, cable_specs=None, is_offs
|
|||||||
try:
|
try:
|
||||||
# 找到通往升压站的最短路径上的下一个节点
|
# 找到通往升压站的最短路径上的下一个节点
|
||||||
path = nx.shortest_path(graph, source=node, target='substation')
|
path = nx.shortest_path(graph, source=node, target='substation')
|
||||||
if len(path) > 1:
|
if len(path) > 1: # path[0]是node自己,path[1]是父节点
|
||||||
parent = path[1] # path[0]是node自己,path[1]是父节点
|
parent = path[1]
|
||||||
power_flow[parent] += power_flow[node]
|
power_flow[parent] += power_flow[node]
|
||||||
except nx.NetworkXNoPath:
|
except nx.NetworkXNoPath:
|
||||||
pass
|
pass
|
||||||
@@ -865,7 +861,7 @@ def export_all_scenarios_to_excel(results, filename):
|
|||||||
# 2. 每个方案的详细 Sheet
|
# 2. 每个方案的详细 Sheet
|
||||||
for res in results:
|
for res in results:
|
||||||
# 清理 Sheet 名称
|
# 清理 Sheet 名称
|
||||||
safe_name = res['name'].replace(':', '').replace('/', '-').replace('\\', '-')
|
safe_name = res['name'].replace(':', '').replace('/', '-').replace('\\', '-').replace(' ', '_')
|
||||||
# 截断过长的名称 (Excel限制31字符)
|
# 截断过长的名称 (Excel限制31字符)
|
||||||
if len(safe_name) > 25:
|
if len(safe_name) > 25:
|
||||||
safe_name = safe_name[:25]
|
safe_name = safe_name[:25]
|
||||||
@@ -1004,11 +1000,13 @@ def visualize_design(turbines, substation, connections, title, ax=None, show_cos
|
|||||||
return ax
|
return ax
|
||||||
|
|
||||||
# 7. 主函数:比较两种设计方法
|
# 7. 主函数:比较两种设计方法
|
||||||
def compare_design_methods(excel_path=None, n_clusters_override=None):
|
def compare_design_methods(excel_path=None, n_clusters_override=None, interactive=True, plot_results=True):
|
||||||
"""
|
"""
|
||||||
比较MST和三种电缆方案下的K-means设计方法
|
比较MST和三种电缆方案下的K-means设计方法
|
||||||
:param excel_path: Excel文件路径
|
:param excel_path: Excel文件路径
|
||||||
:param n_clusters_override: 可选,手动指定簇的数量
|
:param n_clusters_override: 可选,手动指定簇的数量
|
||||||
|
:param interactive: 是否启用交互式导出 (CLI模式)
|
||||||
|
:param plot_results: 是否生成和保存对比图表
|
||||||
"""
|
"""
|
||||||
cable_specs = None
|
cable_specs = None
|
||||||
if excel_path:
|
if excel_path:
|
||||||
@@ -1018,7 +1016,7 @@ def compare_design_methods(excel_path=None, n_clusters_override=None):
|
|||||||
scenario_title = "Offshore Wind Farm (Imported Data)"
|
scenario_title = "Offshore Wind Farm (Imported Data)"
|
||||||
except Exception:
|
except Exception:
|
||||||
print("回退到自动生成数据模式...")
|
print("回退到自动生成数据模式...")
|
||||||
return compare_design_methods(excel_path=None, n_clusters_override=n_clusters_override)
|
return compare_design_methods(excel_path=None, n_clusters_override=n_clusters_override, interactive=interactive, plot_results=plot_results)
|
||||||
else:
|
else:
|
||||||
print("正在生成海上风电场数据 (规则阵列布局)...")
|
print("正在生成海上风电场数据 (规则阵列布局)...")
|
||||||
turbines, substation = generate_wind_farm_data(n_turbines=30, layout='grid', spacing=800)
|
turbines, substation = generate_wind_farm_data(n_turbines=30, layout='grid', spacing=800)
|
||||||
@@ -1029,7 +1027,12 @@ def compare_design_methods(excel_path=None, n_clusters_override=None):
|
|||||||
# 准备三种电缆方案
|
# 准备三种电缆方案
|
||||||
# 原始 specs 是 5 元素元组: (section, capacity, resistance, cost, is_optional)
|
# 原始 specs 是 5 元素元组: (section, capacity, resistance, cost, is_optional)
|
||||||
# 下游函数期望 4 元素元组: (section, capacity, resistance, cost)
|
# 下游函数期望 4 元素元组: (section, capacity, resistance, cost)
|
||||||
|
has_optional_cables = False
|
||||||
|
|
||||||
if cable_specs:
|
if cable_specs:
|
||||||
|
# 检查是否存在 Optional 为 Y 的电缆
|
||||||
|
has_optional_cables = any(s[4] for s in cable_specs)
|
||||||
|
|
||||||
# 方案 1: 不含 Optional='Y' (Standard)
|
# 方案 1: 不含 Optional='Y' (Standard)
|
||||||
specs_1 = [s[:4] for s in cable_specs if not s[4]]
|
specs_1 = [s[:4] for s in cable_specs if not s[4]]
|
||||||
|
|
||||||
@@ -1049,25 +1052,34 @@ def compare_design_methods(excel_path=None, n_clusters_override=None):
|
|||||||
specs_1 = default_specs
|
specs_1 = default_specs
|
||||||
specs_2 = default_specs
|
specs_2 = default_specs
|
||||||
specs_3 = default_specs[:-1]
|
specs_3 = default_specs[:-1]
|
||||||
|
# 默认库视为没有 optional
|
||||||
|
has_optional_cables = False
|
||||||
|
|
||||||
scenarios = [
|
scenarios = [
|
||||||
("Scenario 1 (Standard)", specs_1),
|
("Scenario 1 (Standard)", specs_1)
|
||||||
("Scenario 2 (With Optional)", specs_2),
|
|
||||||
("Scenario 3 (No Max)", specs_3)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if has_optional_cables:
|
||||||
|
scenarios.append(("Scenario 2 (With Optional)", specs_2))
|
||||||
|
scenarios.append(("Scenario 3 (No Max)", specs_3))
|
||||||
|
else:
|
||||||
|
# 重新编号,保证连续性
|
||||||
|
scenarios.append(("Scenario 2 (No Max)", specs_3))
|
||||||
|
|
||||||
# 1. MST 方法作为基准 (使用 Scenario 1)
|
# 1. MST 方法作为基准 (使用 Scenario 1)
|
||||||
mst_connections = design_with_mst(turbines, substation)
|
mst_connections = design_with_mst(turbines, substation)
|
||||||
mst_evaluation = evaluate_design(turbines, mst_connections, substation, 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")
|
||||||
|
|
||||||
# 准备画布 2x2
|
# 准备画布 2x2
|
||||||
fig, axes = plt.subplots(2, 2, figsize=(20, 18))
|
fig = None
|
||||||
axes = axes.flatten()
|
axes = []
|
||||||
|
if plot_results:
|
||||||
# 绘制 MST
|
fig, axes = plt.subplots(2, 2, figsize=(20, 18))
|
||||||
visualize_design(turbines, substation, mst_evaluation['details'],
|
axes = axes.flatten()
|
||||||
f"MST Method (Standard Cables)\nTotal Cost: ¥{mst_evaluation['total_cost']/10000:.2f}万",
|
# 绘制 MST
|
||||||
ax=axes[0])
|
visualize_design(turbines, substation, mst_evaluation['details'],
|
||||||
|
f"MST Method (Standard Cables)\nTotal Cost: ¥{mst_evaluation['total_cost']/10000:.2f}万",
|
||||||
|
ax=axes[0])
|
||||||
|
|
||||||
print(f"\n===== 开始比较电缆方案 =====")
|
print(f"\n===== 开始比较电缆方案 =====")
|
||||||
|
|
||||||
@@ -1184,15 +1196,17 @@ def compare_design_methods(excel_path=None, n_clusters_override=None):
|
|||||||
|
|
||||||
# 可视化 (只画 Base 版本)
|
# 可视化 (只画 Base 版本)
|
||||||
ax_idx = i + 1
|
ax_idx = i + 1
|
||||||
if ax_idx < 4:
|
if plot_results and ax_idx < 4:
|
||||||
n_circuits = turbines_base['cluster'].nunique()
|
n_circuits = turbines_base['cluster'].nunique()
|
||||||
title = f"{base_name} ({n_circuits} circuits)\nCost: ¥{eval_base['total_cost']/10000:.2f}万"
|
title = f"{base_name} ({n_circuits} circuits)\nCost: ¥{eval_base['total_cost']/10000:.2f}万"
|
||||||
visualize_design(turbines_base, substation, eval_base['details'], title, ax=axes[ax_idx])
|
visualize_design(turbines_base, substation, eval_base['details'], title, ax=axes[ax_idx])
|
||||||
|
|
||||||
plt.tight_layout()
|
if plot_results:
|
||||||
output_filename = 'wind_farm_design_comparison.png'
|
plt.tight_layout()
|
||||||
plt.savefig(output_filename, dpi=300)
|
output_filename = 'wind_farm_design_comparison.png'
|
||||||
print(f"\n比较图(Base版)已保存至: {output_filename}")
|
plt.savefig(output_filename, dpi=300)
|
||||||
|
plt.close()
|
||||||
|
print(f"\n比较图(Base版)已保存至: {output_filename}")
|
||||||
|
|
||||||
# 准备文件路径
|
# 准备文件路径
|
||||||
if excel_path:
|
if excel_path:
|
||||||
@@ -1208,6 +1222,10 @@ def compare_design_methods(excel_path=None, n_clusters_override=None):
|
|||||||
if comparison_results:
|
if comparison_results:
|
||||||
export_all_scenarios_to_excel(comparison_results, excel_out_filename)
|
export_all_scenarios_to_excel(comparison_results, excel_out_filename)
|
||||||
|
|
||||||
|
if not interactive:
|
||||||
|
print(f"非交互模式:已自动导出 Excel 对比报表: {excel_out_filename}")
|
||||||
|
return comparison_results
|
||||||
|
|
||||||
# 交互式选择导出 DXF
|
# 交互式选择导出 DXF
|
||||||
print("\n===== 方案选择 =====")
|
print("\n===== 方案选择 =====")
|
||||||
best_idx = 0
|
best_idx = 0
|
||||||
@@ -1240,13 +1258,25 @@ def compare_design_methods(excel_path=None, n_clusters_override=None):
|
|||||||
choice = best_idx
|
choice = best_idx
|
||||||
|
|
||||||
selected_res = comparison_results[choice]
|
selected_res = comparison_results[choice]
|
||||||
print(f"正在导出 '{selected_res['name']}' 到 DXF: {dxf_filename} ...")
|
|
||||||
export_to_dxf(selected_res['turbines'], substation, selected_res['eval']['details'], dxf_filename)
|
# 生成带方案名称的文件名
|
||||||
|
base_dxf_name, ext = os.path.splitext(dxf_filename)
|
||||||
|
safe_suffix = selected_res['name'].replace(' ', '_').replace(':', '').replace('(', '').replace(')', '').replace('/', '-')
|
||||||
|
final_filename = f"{base_dxf_name}_{safe_suffix}{ext}"
|
||||||
|
|
||||||
|
print(f"正在导出 '{selected_res['name']}' 到 DXF: {final_filename} ...")
|
||||||
|
export_to_dxf(selected_res['turbines'], substation, selected_res['eval']['details'], final_filename)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"输入处理出错: {e},将使用默认推荐方案。")
|
print(f"输入处理出错: {e},将使用默认推荐方案。")
|
||||||
selected_res = comparison_results[best_idx]
|
selected_res = comparison_results[best_idx]
|
||||||
print(f"正在导出 '{selected_res['name']}' 到 DXF: {dxf_filename} ...")
|
|
||||||
export_to_dxf(selected_res['turbines'], substation, selected_res['eval']['details'], dxf_filename)
|
# 生成带方案名称的文件名
|
||||||
|
base_dxf_name, ext = os.path.splitext(dxf_filename)
|
||||||
|
safe_suffix = selected_res['name'].replace(' ', '_').replace(':', '').replace('(', '').replace(')', '').replace('/', '-')
|
||||||
|
final_filename = f"{base_dxf_name}_{safe_suffix}{ext}"
|
||||||
|
|
||||||
|
print(f"正在导出 '{selected_res['name']}' 到 DXF: {final_filename} ...")
|
||||||
|
export_to_dxf(selected_res['turbines'], substation, selected_res['eval']['details'], final_filename)
|
||||||
|
|
||||||
return comparison_results
|
return comparison_results
|
||||||
|
|
||||||
@@ -1260,4 +1290,4 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# 3. 运行比较
|
# 3. 运行比较
|
||||||
# 如果没有提供excel文件,将自动回退到生成数据模式
|
# 如果没有提供excel文件,将自动回退到生成数据模式
|
||||||
compare_design_methods(args.excel, n_clusters_override=args.clusters)
|
compare_design_methods(args.excel, n_clusters_override=args.clusters, interactive=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user