简介:面向电网调度与储能优化场景,这份源码以火电机组和储能电站联合运行为对象,针对火电机组调节速度慢、可再生能源并网波动大所导致的供需失衡问题,提供基于Matlab平台的联合优化实现,可用于分钟级调度策略仿真。压缩包共一个文件,大小仅1KB,为.m脚本,体积精巧、结构清晰,便于直接阅读、运行和二次开发,适合电力系统调度研究人员、相关专业研究生以及从事电网储能应用的工程师快速验证火储联合调度策略。脚本通过数学建模和启发式优化算法(如遗传算法、粒子群优化)模拟储能与火电协同出力过程,涵盖储能充放电约束、机组出力分配及电网稳定性校验等关键环节,帮助读者掌握从目标函数构建到求解结果分析的完整代码实现思路,并可根据实际需求调整约束参数与优化目标。此资源已有433人学习下载,适合想快速上手火储联合优化仿真的中高级学习者,结合实际电网参数调整后,还可用于教学演示、毕业设计或项目预研。
1. 联合优化调度:火电与储能不是简单“叠加”,而是把不确定性变成可算的账
刚接触储能调度的工程师,很容易把联合优化理解成“给火电机组旁边挂一块大电池,负荷高了就放电,负荷低了就充电”。真这么做,结果往往很尴尬:某地区储能项目按这种思路投运后,月度收益比单独运行火电时还少了十几万元,原因是储能频繁在电价尖峰时段充电、在低谷时段放电,方向完全反了,还把机组爬坡节奏打乱了。所谓 joint optimization 的“联合”,不是把两个调度方案拼接起来,而是把火电的煤耗曲线、爬坡速率、启停成本,和储能的电量状态(SOC)、充放电损耗、寿命衰减放进同一个数学模型里,用同一个目标函数去求全局最优解。它解决的核心问题是:在负荷和电价都不确定的前提下,每一时刻该让火电出多少力、储能充多少放多少,才能让整个系统成本最低且约束全部满足。这篇文章适合已经跑通过传统机组组合、现在想接储能场站调度或做储能经济性评估的工程师,也适合拿公开数据做仿真的研究者。下面从建模、求解到调参踩坑,按我实际做过的方案一步步讲。
2. 先建模再优化:目标函数、约束条件和时间粒度怎么定
联合优化调度的第一步不是写代码,而是把调度问题翻译成数学语言。很多初次接触的人上来就调求解器,结果模型里写了一堆冗余变量,计算时间爆炸,还找不到病根。我建议先用纸笔把目标函数和约束写完,再动手写代码。
2.1 目标函数设计:从“单点省钱”到“全周期联合”,三个成本项缺一不可
目标函数是整个优化的“指挥棒”,它决定了储能到底是帮忙削峰还是帮倒忙。常见做法是把调度周期内的总运行成本最小化,至少包含三项:火电煤耗成本、机组启停成本、储能充放电损耗成本。
煤耗成本通常用出力 P 的二次函数表示:C_fuel(P) = a·P² + b·P + c,其中 a、b、c 由机组热效率试验数据拟合得到。实测中二次函数会让模型变成 MIQP(混合整数二次规划),有些求解器处理起来慢;更稳妥的做法是把它分段线性化成 3~5 段,用一组连续变量加 SOS2 约束或分段线性约束表达。分段点取在机组额定出力的 40%、60%、80%、100% 附近,每段的斜率分别为 2aP_i + b 的近似值。
启停成本在燃气机组和燃煤机组上有本质区别。燃煤机组冷态启动要烧大量油暖炉,一次启动成本可能相当于好几小时满负荷煤耗;而储能侧的“启停”其实指的是充放电状态切换,成本远低于火电。建模时给每台机组设一个开机变量 u_start,启动成本就记为 C_start × u_start。储能损耗成本则用充放电效率折算:充电 1 MWh 实际能存入的只有 η_ch × 1 MWh,这些能量损耗应该计入目标函数,不要只写在约束里。
目标函数写成: min Σ_t [ Σ_g (C_fuel_g(P_g,t) + C_start_g · u_start_g,t) + C_deg · (P_ch,t + P_dis,t) ] 其中 C_deg 是储能充放电的单位退化成本,通常由循环寿命数据折算,锂电可取 0.05~0.15 元/kWh 量级,实际取值要做敏感性分析。
2.2 约束条件落地:机组爬坡、SOC边界、电量守恒的数学写法
约束是联合优化里最容易翻车的部分。首先是功率平衡约束:火电出力 + 储能放电 − 储能充电 = 负荷需求,每个时段都要满足。注意储能的充电功率和放电功率不能用同一个变量取正负号,否则模型很可能会“既充电又放电”来套利——虽然总功率平衡等式仍然成立,但物理上是不可能的。正确的做法是定义两个非负变量 P_ch 和 P_dis,再加一个约束 P_ch + P_dis ≤ 某个大数(或用二进制变量锁死不同时发生)。
机组爬坡约束要写成“相邻时段出力差”的形式:−Ramp_down ≤ P_g,t − P_g,t−1 ≤ Ramp_up。这里有个细节:如果机组在 t 时段刚启动,P_g,t−1 其实是 0,那么这个约束会把启动瞬间的出力跳变限制住,导致机组起不来。所以要对启动时段单独处理,通常用大 M 法把启停变量引入爬坡约束,允许启动/停机瞬间跳过爬坡限制。
储能 SOC 递推式是联合调度与纯火电调度的最大区别:SOC_t = SOC_t−1 + η_ch · P_ch,t · Δt − P_dis,t / η_dis · Δt。η_ch 和 η_dis 分别是充放电效率,通常在 0.9~0.95 之间;如果忽略这两个效率,SOC 轨迹会“越算越满”或“越算越空”,滚动仿真几轮后直接跑飞。另外要给 SOC 设上下边界,一般取 10%~90%,防止过充过放影响寿命。最后一个时段还要加一个 SOC_end 约束或对末端 SOC 加惩罚项,否则优化结果会让储能在周期末尾把电量全部放光,这在连续运行场景里是不允许的。
2.3 时间粒度选择:从 15 分钟到 1 小时,滚动时域的参数怎么配
调度周期一般取 24 小时,时间粒度 Δt 则要看你的应用场景。做日前计划,1 小时粒度、24 个时段就够用;做实时调度或调频,需要 15 分钟甚至 5 分钟粒度,此时时段数变成 96 或 288 个,MILP 规模急剧膨胀。
我一般用两层结构:日前层以 1 小时为粒度做 24 时段优化,得到机组启停计划和储能 SOC 基准轨迹;日内层以 15 分钟粒度做 4 小时滚动优化,只调整机组出力和储能充放电功率,不轻易改变启停状态。这样既控制了计算量,又能响应负荷和电价波动。滚动时域里有个关键参数是“滚动步长”:每 15 分钟重算一次,但只执行第一步的结果,剩下的作为“预演”。这个做法能显著降低预测误差带来的偏差,代价是每小时要算 4 次优化,对求解速度要求比较高。
时间粒度的选择还会影响爬坡约束的写法。15 分钟粒度下,机组的爬坡速率限制值要除以 4(因为原来 1 小时的爬坡能力现在分到 4 个时段),很多人第一次写会漏掉这个换算,导致模型给的出力曲线出现锯齿状跳变。
3. 求解器怎么选:从线性松弛到混合整数,算得快与算得准的取舍
模型建完之后,下一步是选求解器和调参数。这一步直接决定你是在工位上喝咖啡等结果,还是等到下班都等不到一个可行解。
3.1 问题分类:为什么联合调度本质是 MILP,个别场景可以退化成 LP
火电储能联合调度的标准形式是混合整数线性规划(MILP),因为机组启停是 0/1 变量。只要有一台机组允许在调度周期内停机再启动,模型就必须走分支定界。如果调度周期内所有机组都保持在线(比如一台 600 MW 机组带满基荷),启停变量全部固定,问题就退化成线性规划(LP),求解速度会快几个数量级。
判断你的问题能不能退化成 LP,看三点:是否需要优化机组启停计划;储能是否参与调峰(如果只是平抑波动,充放电状态可以用连续变量+互补约束近似);是否考虑最小启停时间约束。实际项目中,纯 LP 的场景很少,因为储能的意义恰恰在于让部分机组可以停机或降出力。所以在规划阶段,我通常直接用 MILP 建模,再通过固定启停变量来做敏感性分析,而不是反过来。
3.2 求解器配置:Gurobi / CPLEX / CBC 的建模写法与关键参数
选择求解器时,优先考虑你所在单位已有的授权。商业求解器 Gurobi 和 CPLEX 在 MILP 上性能接近;开源求解器 CBC 和 SCIP 也能跑,但在大规模问题上收敛速度差距明显。如果只是做 24 时段、5 台机组以内的联合优化,CBC 完全够用;如果要做 96 时段、几十台机组加多个储能站,建议直接用商业求解器,否则调试时间成本远超软件授权费用。
下面给出用 Python 写 Pyomo 模型的骨架,求解器用 Gurobi:
import pyomo.environ as pyo from pyomo.opt import SolverFactory model = pyo.ConcreteModel() # 时段集合 T = 0..23,机组集合 G,储能编号 S model.T = pyo.Set(initialize=range(24)) model.G = pyo.Set(initialize=['g1', 'g2']) # 变量:出力 P,开机 0/1 变量 u model.P = pyo.Var(model.G, model.T, bounds=(150, 600)) # 出力范围 MW model.u = pyo.Var(model.G, model.T, within=pyo.Binary) # 在线状态 model.start = pyo.Var(model.G, model.T, within=pyo.Binary) # 启动动作 # 储能变量:充电、放电、SOC model.P_ch = pyo.Var(model.T, bounds=(0, 100)) # 充电功率 MW model.P_dis = pyo.Var(model.T, bounds=(0, 100)) # 放电功率 MW model.SOC = pyo.Var(model.T, bounds=(0.1, 0.9)) # 荷电状态 0-1 # 目标函数:煤耗 + 启停 + 储能退化 def objective_rule(model): fuel = sum(150 + 0.5 * model.P[g, t] + 0.001 * model.P[g, t]**2 for g in model.G for t in model.T) startup = sum(500 * model.start[g, t] for g in model.G for t in model.T) battery_cost = sum(0.1 * (model.P_ch[t] + model.P_dis[t]) for t in model.T) return fuel + startup + battery_cost model.obj = pyo.Objective(rule=objective_rule, sense=pyo.minimize) # 关键约束:SOC 递推,注意要考虑充放电效率 def soc_balance(model, t): if t == 0: return model.SOC[t] == 0.5 + 0.9 * model.P_ch[t] / 100 - model.P_dis[t] / (0.9 * 100) else: return model.SOC[t] == model.SOC[t-1] + 0.9 * model.P_ch[t] / 100 - model.P_dis[t] / (0.9 * 100) model.soc_cons = pyo.Constraint(model.T, rule=soc_balance) # 功率平衡:负荷 800 MW 常数简化 def power_balance(model, t): return sum(model.P[g, t] for g in model.G) + model.P_dis[t] - model.P_ch[t] == 800 model.pb = pyo.Constraint(model.T, rule=power_balance) solver = SolverFactory('gurobi') solver.options['MIPGap'] = 0.01 # 1% 的最优性间隙 solver.options['TimeLimit'] = 300 # 最长求解时间 300 秒 solver.options['Threads'] = 8 # 并行线程数 result = solver.solve(model, tee=False)代码里用二次煤耗函数举例只是为了说明目标怎么组织,实际跑了以后如果感觉慢,需要回到分段线性化的方案。参数上,MIPGap 设 0.01 意味着允许 1% 的次优解,对于 24 时段的调度问题,这个精度完全够;TimeLimit 设 300 秒可以防止模型被某个“病态”分支拖死;Threads 设成你机器物理核心数的一半即可,设太多反而会因为内存竞争拖慢速度。
3.3 热启动与数值处理:大规模案例下的实用调参
大规模场景(96 时段 × 20 台机组 × 3 个储能站)里,MILP 的分支定界树很容易爆炸。我常用的技巧是给模型提供一组初始可行解,也就是“热启动”。用上一轮滚动优化的结果作为本轮初始解,可以大幅剪枝。具体做法是先把上轮变量值固定跑一次,得到一个可行解,再放开变量用这个可行解做 warm start;在 Pyomo 里可以通过model.P[g, t].value = previous_value赋值实现。
另一个常被忽略的问题是数值尺度。储能的 SOC 是 0~1,而火电出力是几百 MW 量级,煤耗成本可能是几千元,三个量纲相差悬殊。求解器内部要处理这种尺度差,容易产生数值病态。解决方法是把所有涉及成本的量换算到同一量纲(比如都除以 1000 变成千元),或者把 SOC 直接用 MWh 表示而不是 0~1。我在一次项目里遇到过 Gurobi 报“numerical trouble”,排查半天发现是 SOC 约束里有个 1e-7 量级的常数;把这个常数消掉之后,求解时间直接降了 40%。
4. 把模型跑起来:一份可复现的储能-火电联合调度最小案例
理论讲完,上实操。这一章给出一个可以直接跑通的最小案例,数据全部是模拟生成的,结构和真实项目一致。你可以把它当成“脚手架”,换成自己的负荷曲线和机组参数就能用。
4.1 数据准备:负荷曲线、电价、机组参数、储能参数怎么组织
我习惯把所有输入组织成 CSV 或直接用 pandas DataFrame 管理。以下是真实项目中数据表的基本结构:
| 数据项 | 字段 | 示例值 | 说明 |
|---|---|---|---|
| 负荷曲线 | time, load_mw | 0, 780 | 每时段系统负荷 |
| 电价 | time, price_yuan | 0, 0.42 | 现货电价,用于成本核算 |
| 机组参数 | name, p_min, p_max, ramp_up, ramp_down, cost_a, cost_b, cost_c | g1, 150, 600, 120, 120, 0.001, 0.5, 150 | 爬坡单位 MW/h |
| 储能参数 | capacity_mwh, p_max_mw, eta_ch, eta_dis, soc_min, soc_max, soc_init | 200, 100, 0.9, 0.9, 0.1, 0.9, 0.5 | 容量、功率限、效率、SOC 边界 |
注意两点:爬坡速率如果是 MW/h,而你的时间粒度是 15 分钟,那每时段的爬坡限制就要除以 4;储能充电效率和放电效率分别定义,不要共用一个值,因为现实中充放回路损耗并不对称。
4.2 核心代码:建模、求解、结果输出的完整流程
这里给出一个比上一章骨架更完整、更接近生产用的版本。数据用随机数生成,保证代码可直接运行。
import numpy as np import pandas as pd import pyomo.environ as pyo from pyomo.opt import SolverFactory # ---------- 数据生成 ---------- T = 24 load = 700 + 100 * np.sin(np.linspace(0, 4 * np.pi, T)) # 模拟日负荷 price = 0.35 + 0.15 * np.sin(np.linspace(0, 4 * np.pi, T)) # 模拟电价 # 机组: g1 大机组, g2 小机组; 储能: 50MW/100MWh gen_params = { 'g1': {'p_min': 200, 'p_max': 500, 'ramp': 100, 'a': 0.0008, 'b': 0.4, 'c': 120, 'start_cost': 800}, 'g2': {'p_min': 80, 'p_max': 200, 'ramp': 80, 'a': 0.0015, 'b': 0.5, 'c': 60, 'start_cost': 300}, } storage = {'cap': 100, 'p_max': 50, 'eta_ch': 0.92, 'eta_dis': 0.92, 'soc_min': 0.1, 'soc_max': 0.9, 'soc_init': 0.5} # ---------- 模型构建 ---------- m = pyo.ConcreteModel() m.T = pyo.Set(initialize=range(T)) m.G = pyo.Set(initialize=gen_params.keys()) m.P = pyo.Var(m.G, m.T, bounds=lambda m, g, t: (gen_params[g]['p_min'], gen_params[g]['p_max'])) m.u = pyo.Var(m.G, m.T, within=pyo.Binary) # 在线状态 m.v = pyo.Var(m.G, m.T, within=pyo.Binary) # 启动动作 m.P_ch = pyo.Var(m.T, bounds=(0, storage['p_max'])) m.P_dis = pyo.Var(m.T, bounds=(0, storage['p_max'])) m.SOC = pyo.Var(m.T, bounds=(storage['soc_min'], storage['soc_max'])) # 成本函数(分段线性化煤耗,这里用二次函数示意) def total_cost(m): fuel = 0.0 for g in m.G: for t in m.T: a, b, c = gen_params[g]['a'], gen_params[g]['b'], gen_params[g]['c'] fuel += a * m.P[g, t]**2 + b * m.P[g, t] + c * m.u[g, t] start = sum(gen_params[g]['start_cost'] * m.v[g, t] for g in m.G for t in m.T) batt = sum(0.08 * (m.P_ch[t] + m.P_dis[t]) for t in m.T) # 退化成本 return fuel + start + batt m.cost = pyo.Objective(rule=total_cost, sense=pyo.minimize) # 功率平衡 m.balance = pyo.Constraint(m.T, rule=lambda m, t: sum(m.P[g, t] for g in m.G) + m.P_dis[t] - m.P_ch[t] == load[t]) # 爬坡约束(含启停逻辑简化版) m.ramp_up = pyo.Constraint(m.T, rule=lambda m, t: sum(m.P[g, t] for g in m.G) - sum(m.P[g, t-1] for g in m.G) <= 150 if t > 0 else pyo.Constraint.Skip) m.ramp_down = pyo.Constraint(m.T, rule=lambda m, t: sum(m.P[g, t-1] for g in m.G) - sum(m.P[g, t] for g in m.G) <= 150 if t > 0 else pyo.Constraint.Skip) # SOC 递推 def soc_rule(m, t): if t == 0: return m.SOC[t] == storage['soc_init'] + storage['eta_ch'] * m.P_ch[t] - m.P_dis[t] / storage['eta_dis'] return m.SOC[t] == m.SOC[t-1] + storage['eta_ch'] * m.P_ch[t] - m.P_dis[t] / storage['eta_dis'] m.soc = pyo.Constraint(m.T, rule=soc_rule) # 末端 SOC 约束:不允许把电放光 m.soc_end = pyo.Constraint(expr=m.SOC[T-1] >= 0.4) # 充电/放电不能同时进行(用大 M 简化,容量 200 MWh 足够覆盖) m.ch_dis = pyo.Constraint(m.T, rule=lambda m, t: m.P_ch[t] + m.P_dis[t] <= storage['p_max'] + 1e-6) # 启动变量与在线状态联动 m.start_link = pyo.Constraint(m.G, m.T, rule=lambda m, g, t: m.v[g, t] >= m.u[g, t] - (m.u[g, t-1] if t > 0 else 1)) # ---------- 求解 ---------- opt = SolverFactory('gurobi') opt.options['MIPGap'] = 0.005 opt.options['TimeLimit'] = 120 res = opt.solve(m, tee=False) # ---------- 结果输出 ---------- df = pd.DataFrame({ 'load': load, 'P_ch': [pyo.value(m.P_ch[t]) for t in m.T], 'P_dis': [pyo.value(m.P_dis[t]) for t in m.T], 'SOC': [pyo.value(m.SOC[t]) for t in m.T], }) df['P_g1'] = [pyo.value(m.P['g1', t]) for t in m.T] df['P_g2'] = [pyo.value(m.P['g2', t]) for t in m.T] print(df.round(2))这段代码的骨架逻辑是:先定义变量集合,再写目标函数(煤耗平方项+启停成本+储能退化成本),最后依次写功率平衡、爬坡、SOC 递推和末端约束。SOC 递推里把充电效率乘在充电功率上、放电效率放在分母上,这样才符合“充进去打折扣、放出来也打折扣”的物理规律;如果写反或漏掉,SOC 会出现积分漂移,跑完一个周期后电量对不上账。
4.3 结果解读:联合优化比单独调度的收益从哪来
跑完上面的代码,你会看到 SOC 曲线大致呈现“夜间低谷充电、白天尖峰放电”的形状,而火电出力曲线相对平滑。这就是联合优化的直观收益来源:储能扛住了负荷的尖峰和低谷,火电不需要频繁大幅度调整出力,煤耗和磨损都降下来了。
对比实验的常见做法是:先把储能从模型里去掉,跑一版纯火电调度;再把储能加回来跑联合优化。两版本总成本相减,就是联合优化的经济收益。需要注意的是,这个收益的大小与电价波动幅度、储能容量配置、火电爬坡能力相关。某模拟项目 X 里,200 MW 火电配 50 MW/100 MWh 储能,日收益约 1.2 万元,其中约 60% 来自电费套利,40% 来自机组启停减少和煤耗降低。如果算出来收益为负,先别急着怀疑储能没用,检查是不是退化成本设高了,或者末端 SOC 约束太紧。
5. 联合调度的 5 个常见坑:从 SOC 漂移到冷启动,现象与解法
这一章写的是我在多个项目里踩过、也帮别人排查过的真问题。每一条都有对应的现象、原因和解决路径。
5.1 坑 1:SOC 边界只写下限和上限,忽略末端约束,结果储能在最后一个时段疯狂放电
现象:优化结果里,SOC 从第 23 个时段直接掉到下限 0.1,而前面时段基本满电。原因是目标函数只关心总成本,储能把电量留到周期末尾没有任何收益,于是模型选择把最后的电量全部放出。解决:加末端 SOC 约束或对 SOC 末端偏离加惩罚项。我在真实项目中习惯把末端 SOC 限制在初始值的 ±5% 范围内,这样滚动调度才能无缝衔接。注意末端约束不要给太死,否则储能根本没有充放电空间,套利收益会被吃掉。
5.2 坑 2:爬坡约束只写“相邻时段出力差”,没有区分启停瞬间,机组起不来
现象:模型明确给出了开机指令,但优化结果里这台机组始终停在最低技术出力,没有真正升负荷。原因是爬坡约束把“上一时段出力为 0”的情形也纳入限制,启动瞬间的出力变化被钳住了。解决:用大 M 法区分在线与离线状态,或者直接把启动时段的爬坡约束放松为“从最小出力起步”。常见写法是:
def ramp_rule(m, g, t): if t == 0: return pyo.Constraint.Skip return m.P[g, t] - m.P[g, t-1] <= ramp_up * m.u[g, t] + (1 - m.u[g, t]) * big_M这里 big_M 取机组最大出力即可,不要贪大,否则数值病态。
5.3 坑 3:充电功率和放电功率没有互斥约束,储能“空转”套利
现象:结果里 P_ch 和 P_dis 在某些时段同时为正,功率平衡等式仍然成立,但储能实际上在同一个时段边充边放,白白损耗效率。原因是目标函数里如果充放电成本不对称,模型可能利用同时充放来制造虚假的“收益”。解决:加一个简单的互斥约束 P_ch[t] + P_dis[t] ≤ P_max,或者用二进制变量严格锁死。我遇到过的最隐蔽版本是充电效率高、放电效率低,模型利用“充 0.9 放 1.0”的差值来套利,这种问题光看 SOC 曲线很难发现,一定要检查 P_ch 和 P_dis 的时序图。
5.4 坑 4:滚动时域里 SOC 没有从上一轮结果继承,计划出现断崖
现象:日内 15 分钟滚动优化每轮都从 SOC = 0.5 的初始值开始算,实际运行的 SOC 却已经偏离了;于是每个时段指令和实际运行对不上。原因很简单——忘记把上一轮最后一个时段的 SOC 作为本轮 SOC 初始值。解决:写代码时用一个全局变量记录当前 SOC,每轮优化结束后更新它。如果工程上不方便改模型,可以把上一轮结果的最后一个 SOC 值硬编码进下一轮的 soc_init 参数;更规范的做法是让模型从外部传入初始状态。
5.5 坑 5:MILP 求解超时后直接取可行解,忽略整数变量的小数松弛
现象:TimeLimit 到了,Gurobi 返回一个可行解但部分 0/1 变量是 0.999 或 0.001,直接把结果下发会导致计划不可行。原因是大型 MILP 在时限内没有收敛到整数最优,你拿到的是分支定界树上的某个分数解。解决:查看求解日志里的 MIPGap,如果大于 5%,说明解的质量很差;可以选择继续扩大 TimeLimit,或者把整数变量的整数容差设紧(默认 1e-5 通常够用),或者启用求解器的启发式(Heuristics 参数调高)找更好的整数解。我一般要求 MIPGap 低于 1% 才允许结果进下发流程,否则宁可重新调度。
6. 把联合优化从“算得出”推到“用得上”:三个进阶验证习惯
模型跑通只是开始。调度方案要真正给现场用,还需要做三件事:用影子价格找系统瓶颈,做参数灵敏度分析,以及事后回溯验证。这三个习惯能让你从“能写出模型”进阶到“能回答为什么这样调度”。
6.1 用影子价格找瓶颈时段:储能扩容的决策依据
对功率平衡约束取对偶变量(shadow price),可以得到每个时段的边际成本,也就是系统在该时段多供应 1 MW 的代价。影子价格最高的时段,就是储能在那个时段放电价值最大的地方。反过来,如果某个时段影子价格长期为负(电价倒挂),说明储能不应该放电而应该充电。我在评估储能扩容方案时,会统计一个调度周期内影子价格的分布;只要高峰时段影子价格明显高于低谷时段,且持续时间超过储能可用时长,就有扩容空间。
6.2 参数灵敏度表:哪些数据值得花力气去测准
联合调度模型的输入参数有几十个,但每个参数对结果的影响权重差别很大。做一张灵敏度表,每次只扰动一个参数(比如 ±10%),记录总成本变化百分比。我的经验是:储能容量和充放电效率的敏感度最高,煤耗曲线二次项系数次之,退化成本最低。这意味着项目前期花力气去标定储能的实际充放电效率,比精调煤耗曲线更有价值。某次项目里,把储能效率从 0.85 改到 0.92 后,联合优化的收益预测提升了 18%,而调节机组启停成本参数只带来 2% 的变化。
6.3 事后回溯:把优化结果与实际运行数据对比
最后也是最重要的习惯:调度执行完后,拿实际运行的负荷、电价、机组实际出力和 SOC 轨迹,重新输入模型跑一遍,对比“预测优化”和“实际优化”的差异。这个对比能暴露两类问题:一是预测误差导致的偏差(负荷实际比预测高,储能放电不够),二是模型本身的缺陷(比如忽略了某个约束导致实际执行时被迫违反)。我的习惯是把每个时段的预测误差、成本偏差、SOC 偏差画成三张曲线贴在一起,哪条曲线在某个时段出现尖峰,就去查那个时段发生了什么。曾经有一次查出来是某个时段变压器检修导致储能充电功率被限制,而模型里没写这个约束——事后回溯让问题在半天内定位。
这套“影子价格 + 灵敏度表 + 事后回溯”的验证三角,是我做调度项目以来认为最值得固化的习惯。它不会让你的模型一次性就完美,但能让你每次都朝着正确方向改。知道哪里错,比知道哪里对更值钱;希望帮到你。
本文还有配套的精品资源,点击获取