""" 多能互补系统储能容量优化可视化程序 该程序绘制负荷曲线、发电曲线和储能出力曲线,直观展示系统运行状态。 作者: 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_curtail_wind = sum(result['curtailed_wind']) total_curtail_solar = sum(result['curtailed_solar']) print(f"\n=== 弃风弃光统计 ===") print(f"总弃风电量: {total_curtail_wind:.2f} MWh") print(f"总弃光电量: {total_curtail_solar:.2f} MWh") # 计算购电和上网电量统计 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") # 计算新能源统计信息 total_solar_potential = sum(solar_output) total_wind_potential = sum(wind_output) total_renewable_potential = total_solar_potential + total_wind_potential total_renewable_actual = total_solar_potential - total_curtail_solar + total_wind_potential - total_curtail_wind # 新能源利用率 = 实际发电量 / 潜在发电量 renewable_utilization_rate = (total_renewable_actual / total_renewable_potential * 100) if total_renewable_potential > 0 else 0 # 新能源消纳电量占比 = 新能源实际发电量 / 总负荷 renewable_consumption_ratio = (total_renewable_actual / sum(load_demand) * 100) if sum(load_demand) > 0 else 0 print(f"\n=== 新能源统计 ===") print(f"新能源潜在发电量: {total_renewable_potential:.2f} MWh") print(f" - 光伏潜在发电量: {total_solar_potential:.2f} MWh") print(f" - 风电潜在发电量: {total_wind_potential:.2f} MWh") print(f"新能源实际发电量: {total_renewable_actual:.2f} MWh") print(f" - 光伏实际发电量: {total_solar_potential - total_curtail_solar:.2f} MWh") print(f" - 风电实际发电量: {total_wind_potential - total_curtail_wind:.2f} MWh") print(f"新能源利用率: {renewable_utilization_rate:.2f}%") print(f"新能源消纳电量占比: {renewable_consumption_ratio:.2f}%") def export_results_to_excel(solar_output, wind_output, thermal_output, load_demand, result, params, filename=None): """ 将多能互补系统储能优化结果导出到Excel文件,包含运行数据、统计结果和系统参数。 Args: solar_output (list): 光伏出力曲线 (MW) wind_output (list): 风电出力曲线 (MW) thermal_output (list): 火电出力曲线 (MW) load_demand (list): 负荷需求曲线 (MW) result (dict): 包含以下键的优化结果字典: - charge_profile: 储能充电功率曲线 (MW) - discharge_profile: 储能放电功率曲线 (MW) - storage_profile: 储能状态曲线 (MWh) - curtailed_wind: 弃风功率曲线 (MW) - curtailed_solar: 弃光功率曲线 (MW) - grid_feed_in: 电网交互功率曲线 (MW, 负值表示购电) - required_storage_capacity: 所需储能总容量 (MWh) - total_curtailment_wind_ratio: 总弃风率 - total_curtailment_solar_ratio: 总弃光率 - total_grid_feed_in_ratio: 总上网电量比例 - energy_balance_check: 能量平衡校验结果 - capacity_limit_reached: 容量限制是否达到 params (object): 系统参数对象,包含各种技术参数 filename (str, optional): 输出文件名,如未提供则自动生成 Returns: str: 生成的Excel文件路径 生成的Excel文件包含以下工作表: - 运行数据: 小时级运行数据 - 统计结果: 关键性能指标统计 - 系统参数: 输入参数汇总 - 说明: 文件使用说明 """ """ 将优化结果导出到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 == '--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 ) # 显示当前使用的系统参数 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 --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()