Files
multi_energy_complementarity/main.py
2025-12-26 02:28:54 +08:00

595 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
多能互补系统储能容量优化可视化程序
该程序绘制负荷曲线、发电曲线和储能出力曲线,直观展示系统运行状态。
作者: iFlow CLI
创建日期: 2025-12-25
"""
import matplotlib
matplotlib.use('TkAgg') # 设置为TkAgg后端以支持图形窗口显示
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from datetime import datetime
from storage_optimization import optimize_storage_capacity, SystemParameters
from excel_reader import read_excel_data, create_excel_template, analyze_excel_data
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
def plot_system_curves(solar_output, wind_output, thermal_output, load_demand, result, show_window=False, display_only=False):
"""
绘制系统运行曲线
Args:
solar_output: 光伏出力曲线 (MW) - 支持24小时或8760小时
wind_output: 风电出力曲线 (MW) - 支持24小时或8760小时
thermal_output: 火电出力曲线 (MW) - 支持24小时或8760小时
load_demand: 负荷曲线 (MW) - 支持24小时或8760小时
result: 优化结果字典
show_window: 是否显示图形窗口
display_only: 是否只显示不保存文件
"""
import matplotlib.pyplot as plt
import numpy as np
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
hours = np.arange(len(solar_output))
data_length = len(solar_output)
# 确定图表标题和采样率
if data_length == 8760:
title_suffix = " (全年8760小时)"
# 对于全年数据我们采样显示每6小时显示一个点
sample_rate = 6
sampled_hours = hours[::sample_rate]
sampled_solar = solar_output[::sample_rate]
sampled_wind = wind_output[::sample_rate]
sampled_thermal = thermal_output[::sample_rate]
sampled_load = load_demand[::sample_rate]
sampled_storage = result['storage_profile'][::sample_rate]
sampled_charge = result['charge_profile'][::sample_rate]
sampled_discharge = result['discharge_profile'][::sample_rate]
sampled_grid_feed_in = result['grid_feed_in'][::sample_rate]
else:
title_suffix = " (24小时)"
sampled_hours = hours
sampled_solar = solar_output
sampled_wind = wind_output
sampled_thermal = thermal_output
sampled_load = load_demand
sampled_storage = result['storage_profile']
sampled_charge = result['charge_profile']
sampled_discharge = result['discharge_profile']
sampled_grid_feed_in = result['grid_feed_in']
# 创建图形4个子图
fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(14, 16))
fig.suptitle('多能互补系统24小时运行曲线', fontsize=16, fontweight='bold')
# === 第一个子图:发电和负荷曲线 ===
ax1.plot(sampled_hours, sampled_load, 'r-', linewidth=2, label='负荷需求')
ax1.plot(sampled_hours, sampled_thermal, 'b-', linewidth=2, label='火电出力')
ax1.plot(sampled_hours, sampled_wind, 'g-', linewidth=2, label='风电出力')
ax1.plot(sampled_hours, sampled_solar, 'orange', linewidth=2, label='光伏出力')
# 计算总发电量
total_generation = [sampled_thermal[i] + sampled_wind[i] + sampled_solar[i] for i in range(len(sampled_thermal))]
ax1.plot(sampled_hours, total_generation, 'k--', linewidth=1.5, alpha=0.7, label='总发电量')
ax1.set_xlabel('时间 (小时)')
ax1.set_ylabel('功率 (MW)')
ax1.set_title(f'发电与负荷曲线{title_suffix}')
ax1.legend(loc='upper right')
ax1.grid(True, alpha=0.3)
ax1.set_xlim(0, max(sampled_hours))
# === 第二个子图:储能充放电曲线 ===
discharge_power = [-x for x in sampled_discharge] # 放电显示为负值
ax2.bar(sampled_hours, sampled_charge, color='green', alpha=0.7, label='充电功率')
ax2.bar(sampled_hours, discharge_power, color='red', alpha=0.7, label='放电功率')
ax2.set_xlabel('时间 (小时)')
ax2.set_ylabel('功率 (MW)')
ax2.set_title(f'储能充放电功率{title_suffix}')
ax2.legend(loc='upper right')
ax2.grid(True, alpha=0.3)
ax2.set_xlim(0, max(sampled_hours))
ax2.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
# === 第三个子图:储能状态曲线 ===
ax3.plot(sampled_hours, sampled_storage, 'b-', linewidth=1, marker='o', markersize=2)
ax3.fill_between(sampled_hours, 0, sampled_storage, alpha=0.3, color='blue')
ax3.set_xlabel('时间 (小时)')
ax3.set_ylabel('储能容量 (MWh)')
ax3.set_title(f'储能状态 (总容量: {result["required_storage_capacity"]:.2f} MWh){title_suffix}')
ax3.grid(True, alpha=0.3)
ax3.set_xlim(0, max(sampled_hours))
ax3.set_ylim(bottom=0)
# === 第四个子图:购电量和上网电量曲线 ===
# 直接使用原始数据:正值表示上网电量,负值表示购电量
grid_power = sampled_grid_feed_in
# 绘制电网交互电量(正值上网,负值购电)
colors = ['brown' if x >= 0 else 'purple' for x in grid_power]
ax4.bar(sampled_hours, grid_power, color=colors, alpha=0.7, label='电网交互电量')
# 添加零线
ax4.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
# 添加图例说明
from matplotlib.patches import Patch
legend_elements = [
Patch(facecolor='brown', alpha=0.7, label='上网电量 (+)'),
Patch(facecolor='purple', alpha=0.7, label='购电量 (-)')
]
ax4.legend(handles=legend_elements, loc='upper right')
ax4.set_xlabel('时间 (小时)')
ax4.set_ylabel('功率 (MW)')
ax4.set_title(f'电网交互电量{title_suffix} (正值:上网, 负值:购电)')
ax4.grid(True, alpha=0.3)
ax4.set_xlim(0, max(sampled_hours))
# 调整布局
plt.tight_layout()
# 根据参数决定是否保存和显示图形
if display_only:
# 只显示,不保存
try:
plt.show()
except Exception as e:
print(f"无法显示图形窗口:{str(e)}")
else:
# 保存图片
plt.savefig('system_curves.png', dpi=300, bbox_inches='tight')
# 根据参数决定是否显示图形窗口
if show_window:
try:
plt.show()
except Exception as e:
print(f"无法显示图形窗口:{str(e)}")
print("图形已保存为 'system_curves.png'")
else:
plt.close() # 关闭图形,不显示窗口
# 打印统计信息
print("\n=== 系统运行统计 ===")
print(f"所需储能总容量: {result['required_storage_capacity']:.2f} MWh")
print(f"最大储能状态: {max(result['storage_profile']):.2f} MWh")
print(f"最小储能状态: {min(result['storage_profile']):.2f} MWh")
print(f"总充电量: {sum(result['charge_profile']):.2f} MWh")
print(f"总放电量: {sum(result['discharge_profile']):.2f} MWh")
print(f"弃风率: {result['total_curtailment_wind_ratio']:.3f}")
print(f"弃光率: {result['total_curtailment_solar_ratio']:.3f}")
print(f"上网电量比例: {result['total_grid_feed_in_ratio']:.3f}")
# 计算购电和上网电量统计
total_grid_feed_in = sum(result['grid_feed_in'])
total_grid_purchase = sum(-x for x in result['grid_feed_in'] if x < 0) # 购电量
total_grid_feed_out = sum(x for x in result['grid_feed_in'] if x > 0) # 上网电量
print(f"\n=== 电网交互统计 ===")
if total_grid_feed_in >= 0:
print(f"净上网电量: {total_grid_feed_in:.2f} MWh")
else:
print(f"净购电量: {-total_grid_feed_in:.2f} MWh")
print(f"总购电量: {total_grid_purchase:.2f} MWh")
print(f"总上网电量: {total_grid_feed_out:.2f} MWh")
def export_results_to_excel(solar_output, wind_output, thermal_output, load_demand, result, params, filename=None):
"""
将优化结果导出到Excel文件
Args:
solar_output: 光伏出力曲线 (MW)
wind_output: 风电出力曲线 (MW)
thermal_output: 火电出力曲线 (MW)
load_demand: 负荷曲线 (MW)
result: 优化结果字典
params: 系统参数
filename: 输出文件名如果为None则自动生成
"""
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"storage_optimization_results_{timestamp}.xlsx"
print(f"\n正在导出结果到Excel文件: {filename}")
# 准备数据
hours = list(range(1, len(solar_output) + 1))
# 分离购电和上网电量
grid_purchase = []
grid_feed_out = []
for power in result['grid_feed_in']:
if power < 0:
grid_purchase.append(-power) # 购电,转换为正值
grid_feed_out.append(0)
else:
grid_purchase.append(0)
grid_feed_out.append(power) # 上网电量
# 创建主要数据DataFrame
data_df = pd.DataFrame({
'小时': hours,
'光伏出力(MW)': solar_output,
'风电出力(MW)': wind_output,
'火电出力(MW)': thermal_output,
'总发电量(MW)': [solar_output[i] + wind_output[i] + thermal_output[i] for i in range(len(solar_output))],
'负荷需求(MW)': load_demand,
'储能充电(MW)': result['charge_profile'],
'储能放电(MW)': result['discharge_profile'],
'储能状态(MWh)': result['storage_profile'],
'弃风量(MW)': result['curtailed_wind'],
'弃光量(MW)': result['curtailed_solar'],
'购电量(MW)': grid_purchase,
'上网电量(MW)': grid_feed_out
})
# 创建统计信息DataFrame
total_grid_feed_in = sum(result['grid_feed_in'])
total_grid_purchase = sum(-x for x in result['grid_feed_in'] if x < 0) # 购电量
total_grid_feed_out = sum(x for x in result['grid_feed_in'] if x > 0) # 上网电量
stats_df = pd.DataFrame({
'指标': [
'所需储能总容量',
'最大储能状态',
'最小储能状态',
'总充电量',
'总放电量',
'弃风率',
'弃光率',
'上网电量比例',
'能量平衡校验',
'净购电量/净上网电量',
'总购电量',
'总上网电量',
'容量限制是否达到'
],
'数值': [
f"{result['required_storage_capacity']:.2f} MWh",
f"{max(result['storage_profile']):.2f} MWh",
f"{min(result['storage_profile']):.2f} MWh",
f"{sum(result['charge_profile']):.2f} MWh",
f"{sum(result['discharge_profile']):.2f} MWh",
f"{result['total_curtailment_wind_ratio']:.3f}",
f"{result['total_curtailment_solar_ratio']:.3f}",
f"{result['total_grid_feed_in_ratio']:.3f}",
"通过" if result['energy_balance_check'] else "未通过",
f"{-total_grid_feed_in:.2f} MWh" if total_grid_feed_in < 0 else f"{total_grid_feed_in:.2f} MWh",
f"{total_grid_purchase:.2f} MWh",
f"{total_grid_feed_out:.2f} MWh",
"" if result['capacity_limit_reached'] else ""
]
})
# 创建系统参数DataFrame
params_df = pd.DataFrame({
'参数名称': [
'最大弃风率',
'最大弃光率',
'最大上网电量比例',
'储能效率',
'放电倍率',
'充电倍率',
'最大储能容量',
'额定火电装机容量',
'额定光伏装机容量',
'额定风电装机容量',
'火电可用发电量',
'光伏可用发电量',
'风电可用发电量'
],
'参数值': [
params.max_curtailment_wind,
params.max_curtailment_solar,
params.max_grid_ratio,
params.storage_efficiency,
params.discharge_rate,
params.charge_rate,
params.max_storage_capacity if params.max_storage_capacity is not None else "无限制",
params.rated_thermal_capacity,
params.rated_solar_capacity,
params.rated_wind_capacity,
params.available_thermal_energy,
params.available_solar_energy,
params.available_wind_energy
],
'单位': [
"比例",
"比例",
"比例",
"效率",
"C-rate",
"C-rate",
"MWh",
"MW",
"MW",
"MW",
"MWh",
"MWh",
"MWh"
]
})
# 写入Excel文件
with pd.ExcelWriter(filename, engine='openpyxl') as writer:
# 写入主要数据
data_df.to_excel(writer, sheet_name='运行数据', index=False)
# 写入统计信息
stats_df.to_excel(writer, sheet_name='统计结果', index=False)
# 写入系统参数
params_df.to_excel(writer, sheet_name='系统参数', index=False)
# 创建说明工作表
description_df = pd.DataFrame({
'项目': [
'文件说明',
'生成时间',
'数据长度',
'数据内容',
'注意事项'
],
'内容': [
'多能互补系统储能容量优化结果',
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
f"{len(solar_output)} 小时",
'包含发电、负荷、储能、弃风弃光、购电上网等完整数据',
'负值表示购电,正值表示上网电量'
]
})
description_df.to_excel(writer, sheet_name='说明', index=False)
print(f"结果已成功导出到: {filename}")
return filename
def generate_yearly_data():
"""生成8760小时的示例数据"""
# 基础日模式
daily_solar = [0.0] * 6 + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0] + [0.0] * 6
daily_wind = [2.0, 3.0, 4.0, 3.0, 2.0, 1.0] * 4
daily_thermal = [5.0] * 24
daily_load = [3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 18.0,
16.0, 14.0, 12.0, 10.0, 8.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 2.0]
# 添加季节性变化
import random
random.seed(42)
yearly_solar = []
yearly_wind = []
yearly_thermal = []
yearly_load = []
for day in range(365):
# 季节性因子(夏季光伏更强,冬季负荷更高)
season_factor = 1.0 + 0.3 * np.sin(2 * np.pi * day / 365)
for hour in range(24):
# 添加随机变化
solar_variation = 1.0 + 0.2 * (random.random() - 0.5)
wind_variation = 1.0 + 0.3 * (random.random() - 0.5)
load_variation = 1.0 + 0.1 * (random.random() - 0.5)
yearly_solar.append(daily_solar[hour] * season_factor * solar_variation)
yearly_wind.append(daily_wind[hour] * wind_variation)
yearly_thermal.append(daily_thermal[hour])
yearly_load.append(daily_load[hour] * (2.0 - season_factor) * load_variation)
return yearly_solar, yearly_wind, yearly_thermal, yearly_load
def main():
"""主函数"""
import sys
# 检查命令行参数
if len(sys.argv) < 2:
print_usage()
return
command = sys.argv[1]
show_window = '--show' in sys.argv # 检查是否包含--show参数
display_only = '--display-only' in sys.argv # 检查是否只显示不保存
if command == '--yearly':
print("生成8760小时全年数据...")
solar_output, wind_output, thermal_output, load_demand = generate_yearly_data()
print(f"数据长度: {len(solar_output)} 小时")
elif command == '--excel':
if len(sys.argv) < 3:
print("错误请指定Excel文件路径")
print("用法python main.py --excel <文件路径>")
return
excel_file = sys.argv[2]
print(f"从Excel文件读取数据{excel_file}")
try:
data = read_excel_data(excel_file, include_parameters=True)
solar_output = data['solar_output']
wind_output = data['wind_output']
thermal_output = data['thermal_output']
load_demand = data['load_demand']
print(f"成功读取{data['data_type']}小时数据")
print(f"原始数据长度:{data['original_length']}小时")
print(f"处理后数据长度:{len(solar_output)}小时")
# 使用Excel中的系统参数
if 'system_parameters' in data:
params = data['system_parameters']
print("\n使用Excel中的系统参数")
print(f" 最大弃风率: {params.max_curtailment_wind}")
print(f" 最大弃光率: {params.max_curtailment_solar}")
print(f" 最大上网电量比例: {params.max_grid_ratio}")
print(f" 储能效率: {params.storage_efficiency}")
print(f" 放电倍率: {params.discharge_rate}")
print(f" 充电倍率: {params.charge_rate}")
print(f" 最大储能容量: {params.max_storage_capacity}")
print(f" 额定火电装机容量: {params.rated_thermal_capacity} MW")
print(f" 额定光伏装机容量: {params.rated_solar_capacity} MW")
print(f" 额定风电装机容量: {params.rated_wind_capacity} MW")
print(f" 火电可用发电量: {params.available_thermal_energy} MWh")
print(f" 光伏可用发电量: {params.available_solar_energy} MWh")
print(f" 风电可用发电量: {params.available_wind_energy} MWh")
else:
print("\n警告:未找到系统参数,使用默认参数")
params = SystemParameters(
max_curtailment_wind=0.1,
max_curtailment_solar=0.1,
max_grid_ratio=0.2,
storage_efficiency=0.9,
discharge_rate=1.0,
charge_rate=1.0,
rated_thermal_capacity=100.0,
rated_solar_capacity=100.0,
rated_wind_capacity=100.0,
available_thermal_energy=2400.0,
available_solar_energy=600.0,
available_wind_energy=1200.0
)
# 显示数据统计
stats = analyze_excel_data(excel_file)
if stats:
print("\n数据统计:")
print(f" 总发电量: {stats['total_generation']:.2f} MW")
print(f" 总负荷: {stats['total_load']:.2f} MW")
print(f" 最大光伏出力: {stats['max_solar']:.2f} MW")
print(f" 最大风电出力: {stats['max_wind']:.2f} MW")
print(f" 最大负荷: {stats['max_load']:.2f} MW")
except Exception as e:
print(f"读取Excel文件失败{str(e)}")
return
elif command == '--create-template':
template_type = sys.argv[2] if len(sys.argv) > 2 else "8760"
template_file = f"data_template_{template_type}.xlsx"
print(f"创建{template_type}小时Excel模板{template_file}")
create_excel_template(template_file, template_type)
return
else:
print("使用24小时示例数据...")
# 示例数据
solar_output = [0.0] * 6 + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0] + [0.0] * 6
wind_output = [2.0, 3.0, 4.0, 3.0, 2.0, 1.0] * 4
thermal_output = [5.0] * 24
load_demand = [3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 18.0,
16.0, 14.0, 12.0, 10.0, 8.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 2.0]
# 使用默认系统参数
params = SystemParameters(
max_curtailment_wind=0.1,
max_curtailment_solar=0.1,
max_grid_ratio=0.2,
storage_efficiency=0.9,
discharge_rate=1.0,
charge_rate=1.0,
rated_thermal_capacity=100.0,
rated_solar_capacity=100.0,
rated_wind_capacity=100.0,
available_thermal_energy=2400.0,
available_solar_energy=600.0,
available_wind_energy=1200.0
)
# 对于 --yearly 参数,也需要设置默认参数
if command == '--yearly':
params = SystemParameters(
max_curtailment_wind=0.1,
max_curtailment_solar=0.1,
max_grid_ratio=0.2,
storage_efficiency=0.9,
discharge_rate=1.0,
charge_rate=1.0,
rated_thermal_capacity=100.0,
rated_solar_capacity=100.0,
rated_wind_capacity=100.0,
available_thermal_energy=2400.0,
available_solar_energy=600.0,
available_wind_energy=1200.0
)
# 显示当前使用的系统参数
print("\n=== 当前使用的系统参数 ===")
print(f"最大弃风率: {params.max_curtailment_wind}")
print(f"最大弃光率: {params.max_curtailment_solar}")
print(f"最大上网电量比例: {params.max_grid_ratio}")
print(f"储能效率: {params.storage_efficiency}")
print(f"放电倍率: {params.discharge_rate}")
print(f"充电倍率: {params.charge_rate}")
print(f"最大储能容量: {params.max_storage_capacity if params.max_storage_capacity is not None else '无限制'}")
print(f"额定火电装机容量: {params.rated_thermal_capacity} MW")
print(f"额定光伏装机容量: {params.rated_solar_capacity} MW")
print(f"额定风电装机容量: {params.rated_wind_capacity} MW")
print(f"火电可用发电量: {params.available_thermal_energy} MWh")
print(f"光伏可用发电量: {params.available_solar_energy} MWh")
print(f"风电可用发电量: {params.available_wind_energy} MWh")
print("=" * 40)
# 计算最优储能容量
print("正在计算最优储能容量...")
result = optimize_storage_capacity(
solar_output, wind_output, thermal_output, load_demand, params
)
# 绘制曲线
print("正在绘制系统运行曲线...")
plot_system_curves(solar_output, wind_output, thermal_output, load_demand, result, show_window, display_only)
# 导出结果到Excel
try:
export_results_to_excel(solar_output, wind_output, thermal_output, load_demand, result, params)
except Exception as e:
print(f"导出Excel文件失败{str(e)}")
if display_only:
print("\n正在显示图形窗口...")
elif show_window:
print("\n曲线图已保存为 'system_curves.png' 并显示图形窗口")
else:
print("\n曲线图已保存为 'system_curves.png'")
def print_usage():
"""打印使用说明"""
print("多能互补系统储能容量优化程序")
print("\n使用方法:")
print(" python main.py --excel <文件路径> # 从Excel文件读取数据")
print(" python main.py --yearly # 使用8760小时全年数据")
print(" python main.py --create-template [类型] # 创建Excel模板(24或8760)")
print(" python main.py # 使用24小时示例数据")
print(" python main.py --show # 显示图形窗口(可与其他参数组合使用)")
print(" python main.py --display-only # 只显示图形窗口,不保存文件")
print("\n示例:")
print(" python main.py --excel data.xlsx")
print(" python main.py --excel data.xlsx --show")
print(" python main.py --excel data.xlsx --display-only")
print(" python main.py --create-template 8760")
print(" python main.py --create-template 24")
print(" python main.py --display-only # 使用示例数据并只显示图形窗口")
if __name__ == "__main__":
main()