做电力系统优化调度的朋友应该都有这种感觉:储能一接入,问题就复杂了好几倍,尤其是当站内储能不是"一台新设备"而是"一批特性各异的电池单元"时,调度策略的构建就更头疼了。"考虑特性分布的储能电站接入的电网多时间尺度源储荷协调调度策略"这个题目,本质上就是在解决两件事:一是储能电站里那些SOH、内阻、自放电率各不相同的电池单体或电池簇,如何在并网之后不拖累电网调度;二是源、储、荷三方在日前、日内、实时三个时间尺度上如何协调,才能既保证运行安全,又把成本压下来。这篇文章我以Matlab代码实现为主线,把整套思路的模型怎么建、约束怎么写、代码怎么搭、坑怎么避,完整拆解一遍。适合正在做储能并网优化、微电网能量管理,或者刚接触多时间尺度调度但又不想只看理论推导的研究生和一线工程师参考。
在往下拆之前,先说明我的一个基本立场:调度策略这种题目,论文里可以写得很漂亮,但落到Matlab代码上,拼的不是花哨的改进算法,而是模型是否可解、数据是否对齐、边界条件是否物理。所以我全文都围绕"能跑的模型"来讲。
1. 为什么要关注储能特性分布与多时间尺度协调
1.1 储能特性分布从哪来,对调度有什么影响
储能电站投运几年后,电池单体之间的差异会越来越明显。即便同一批出厂,运行温度场不均、充放电深度不同、自放电率有差异,都会导致SOH分散。SOH不一致带来的直接后果是:可用容量不像铭牌那样"整齐",若按统一模型调度,某些单元的SOC会先顶到上限,导致整站实际可调功率低于计划值;另一些可能被反复过放,加速老化甚至触发保护。这就是"考虑特性分布"在工程上不是锦上添花,而是保命需求的原因。
在建模时,我不会把所有储能等效成"一个大电池"——这在单台小容量储能的研究里勉强还能接受,但站级储能这样处理,误差会大到让人怀疑人生。更合理的方式是把储能单元按特性分群:SOH相近、内阻相近、温度特性一致的归为一组,每组用一个等效模型描述,再在站控层汇总成调整后的可调功率区间。这样既不会引入几百个电池单体的爆炸式变量,又能保留特性差异对调度边界的影响。可以说,分群等效是"考虑特性分布"落地到优化模型里的桥梁。
例如某个100MW/200MWh的独立储能电站,统计电池簇SOH分布在85%到98%之间。如果按SOH=90%这个平均值来算可用容量,实际运行时SOH=85%的簇会提前达到SOC上限,整站可放功率立刻缩水;而SOH=98%的簇本来还有余量,却因为平均化模型约束了它的出力。分群之后,调度器知道哪几组能多充、哪几组能多放,可调功率区间才真正反映了现场状态。
1.2 多时间尺度到底解决什么问题
电网调度天然是多时间尺度的。日前调度基于预测做24小时发电计划,但风电光伏预测误差在6到24小时尺度上可能达到20%以上;日内滚动预测误差缩小了,但负荷波动依然存在;到了实时运行阶段,分钟级的爬坡和扰动又需要自动发电控制去消纳。储能参与后,问题从"机组跟着负荷走"变成"源、储、荷一起协调",时间尺度不拉开,计算规模会爆炸,时间尺度不衔接,调度指令会打架。
用一个不太精确但好理解的类比:日前计划相当于你早上按天气预报决定穿什么衣服,日内滚动相当于出门前再看一眼窗外再调整,实时调整则是到了楼下感觉冷,临时加件外套。每一层的决策范围不同,但最终目标都是同一个——在不确定性存在的情况下,让系统走得稳、成本低。
我经常被问,为什么不用一个整体模型把所有时间尺度同时优化?答案很简单:第一,变量规模会大到难以求解;第二,预测精度随时间尺度变化,日前不知道15分钟后的细节,实时又必须快速出解。多时间尺度的本质是"渐进式决策":用长时间尺度的粗计划定基调,用短时间尺度的细调整来纠偏。
1.3 源储荷协调为什么比传统"源荷协调"更优
传统调度是源随荷动,机组去追负荷曲线;储能接入后,多了一个可充可放、响应快的调节资源,负荷也不再是被动接受者。源储荷协调的目标,是让发电、储能、柔性负荷三方共同分担不确定性。例如风电大发时,储能充电消纳弃风;负荷高峰时,储能放电削峰,可平移负荷错峰生产;预测偏差来临时,储能和负荷一起响应,机组不需要频繁大幅度爬坡。
在成本上,储能的度电成本虽然不低,但相比让火电机组深度调峰、启停调峰,很多场景下依然划算;负荷侧的需求响应成本更低,只是可调容量往往受限。把它们纳入同一优化框架,本质上是在做一个"多资源组合优化":谁便宜谁先动,谁响应快谁承担短时调节,谁容量大谁做长时支撑。
这种协调在Matlab实现里最终体现为:目标函数里不同资源有不同的成本系数,约束条件里给定各自的调节边界,求解器在同一个模型内自动完成资源排序。你不需要拍脑袋决定"先调储能还是先调负荷",只要系数和边界设得对,优化结果自然会告诉你答案。
2. 数学模型这样建,求解才不会跑偏
2.1 决策变量与时间尺度的对应关系
在建模型之前,先要把决策变量按时间尺度分清楚。我习惯在Matlab中用矩阵维度来区分:
- 日前时间尺度:决策变量是逐时(或15分钟)的机组出力、储能充放电功率、联络线功率、可平移负荷安排,时间维度T_day = 24或96;
- 日内时间尺度:以未来4到6小时为滚动窗口,15分钟一个时段,只对当前窗口内变量做精细优化,已经执行的日前计划作为硬性边界或软约束;
- 实时时间尺度:5分钟甚至1分钟一个时段,决策对象以储能响应和负荷微调为主,机组出力通过爬坡约束限定在日前/日内计划附近,避免机组频繁折腾。
这三种尺度不是三个独立模型,而是同一个模型的"变体"。关键区别在于:滚动窗口大小、预测数据精度、决策变量自由度。我在代码里通常只写一个模型函数,用输入参数控制时间尺度和预测数据,而不是复制三套代码。这样的好处是,修改约束或目标函数时只需改一处,后面调试省很多事。
这里有一个容易被忽视的建模问题:跨时间尺度的变量耦合。例如日内滚动优化时,当前时段之前的SOC、机组出力已经由日前计划决定,不能再自由变化,所以要把它们作为模型参数传进去。如果你把所有变量都定义成sdpvar,求解器会认为历史状态还可以调整,结果就会前后矛盾。我的做法是在模型函数入口用一组init变量传递已确定的状态。
2.2 目标函数如何兼顾经济性与惩罚项
目标函数一般分两大部分:运行成本和惩罚项。运行成本包含火电燃料成本、储能充放电损耗成本、弃风弃光惩罚、需求响应补偿成本。最常见的目标函数是:
min ∑(C_g,i * P_g,i,t + C_ch * P_ch,t + C_dis * P_dis,t + C_cur * P_cur,t + C_load * P_load_shift,t)
其中C_g,i可以是分段线性成本,C_cur表示弃风弃光惩罚系数,这个系数设置很关键。设太低,模型会倾向于大量弃风;设太高,又会让模型为了少弃一点风而让储能过度充放、机组过度调节。我的经验是:惩罚系数不要拍脑袋定,先跑一次不带惩罚的优化,看自然弃风量,再按弃风损失的大致数量级去设惩罚系数,通常比燃料成本高一个到两个数量级即可。
还需要注意,储能充放电同时进行不仅是浪费,还会造成SOC计算混乱。要么引入二进制变量约束互斥,要么用一组约束让同一时段充电功率和放电功率乘积为0。二进制变量在Matlab+Yalmip里就是binvar,但如果时间尺度T很大,二进制变量会拖慢求解速度。工程上,很多情况下可以用一个小量松弛或者直接限定SOC变化方向来规避,但严格起见,我做严肃的算例仿真时还是会保留互斥约束,毕竟多几个binvar换来的是物理合理性。
2.3 约束条件的工程化取舍
约束条件里最核心的是功率平衡约束、储能SOC递推约束、容量与功率上下限约束、机组爬坡约束,以及网络约束。功率平衡约束是等式约束,必须严格满足,否则优化结果不物理。SOC递推约束是:
SOC_i,t+1 = SOC_i,t + η_ch * P_ch,i,t * Δt / E_i - P_dis,i,t * Δt / (η_dis * E_i)
这里有个工程细节:充放电效率η的取值。锂离子电池的充放电效率通常在95%左右,但这是"电池本体"的效率,站级还要算上PCS变流器损耗、变压器损耗和站用电,综合效率可能只有87%-92%。如果你直接用电池本体效率建模,调度出来的结果会偏乐观。我一般会把效率拆成充电效率和放电效率两个参数,放进SOC递推式两侧,这样更接近实际。
网络约束(潮流约束)是最容易让求解器崩溃的部分。如果不做输电网级研究,我倾向于用直流潮流模型替代交流潮流,只保留节点功率平衡、线路潮流限值、相角约束。如果研究的是单节点或辐射状配电网,甚至可以直接简化成不考虑网损的功率平衡,重点是源储荷协调而不是网架。先把核心问题跑通,再逐步加网络约束,这是避免"建模即翻车"的正确路径。
2.4 特性分布如何进入约束边界
前面说特性分群,这里给出更具体的做法。设储能站有N组单元,每组i的特性参数包括:额定容量E_i、当前SOH_i、最大允许充放电功率P_ch_max_i和P_dis_max_i、初始SOC、SOH衰减速率。把SOH乘进容量后,实际可用容量是E_avail,i = SOH_i * E_i。调度时,SOC的上下限也要随之调整,比如考虑到过放风险,低SOH单元的SOC下限要抬高。
更进一步,特性分布不仅影响静态边界,还影响动态特性。比如高内阻单元在同样功率下发热更厉害,温度升高又加速老化。如果要精细刻画,可以在目标函数里加上老化成本项,用SOH衰减率乘以当前单元功率的平方,惩罚"过度压榨"低健康单元。这个做法增加了模型复杂度,但对延长站级寿命很有意义,也是"考虑特性分布"区别于"等效大电池"的真正体现。我在代码里给每个储能单元都留了独立的参数结构体数组,目的就是方便做这种差异化建模。
3. Matlab代码实现的骨架与细节
3.1 为什么用Yalmip而不是手写求解接口
Matlab里做优化调度,主流方案是Yalmip+Cplex/Gurobi。Yalmip是一个建模层,它把优化问题用自然的方式表达出来,再交给底层求解器求解。相比直接用Cplex的Matlab接口手写矩阵A、b、Aeq、beq,Yalmip的代码可读性高太多,尤其当约束条件多、指标变量多的时候,手写矩阵很容易出现行列对不上的低级错误,而且极难排查。我推荐的组合是Yalmip + Gurobi,或者Yalmip + Cplex。Gurobi在混合整数线性规划上通常比Cplex快,遇到MILP规模大的问题更稳。
需要提醒,Yalmip只是建模工具,不负责求解,所以一定要安装底层求解器并配置好路径。很多人以为装了Yalmip就能求解,结果运行时报错没有可用的求解器,这个坑我踩过不止一次。
3.2 核心代码结构与关键片段
我会把整个程序分为几块:参数定义、数据加载、模型构建、求解、结果后处理。下面给出模型构建部分的骨架,以日前调度的MILP模型为例。
%% 参数定义 T = 24; % 时段数,1小时粒度 N_g = 3; % 机组数量 N_s = 4; % 储能单元组数 E_nom = [100 80 120 90]'; % 各单元额定容量/MWh SOH = [0.98 0.93 0.88 0.95]'; E_avail = E_nom .* SOH; % 实际可用容量 P_ch_max = [25 20 30 22]'; % 各单元最大充电功率/MW P_dis_max = [25 20 30 22]'; % 各单元最大放电功率/MW P_g0 = [40 50 30]'; % 机组初始出力/MW SOC0 = [0.50 0.50 0.50 0.50]'; %% 决策变量 P_g = sdpvar(N_g, T, 'full'); P_ch = sdpvar(N_s, T, 'full'); P_dis = sdpvar(N_s, T, 'full'); SOC = sdpvar(N_s, T+1, 'full'); u_ch = binvar(N_s, T, 'full'); % 充电状态0/1 u_dis = binvar(N_s, T, 'full'); % 放电状态0/1 P_cur = sdpvar(T, 1, 'full'); % 弃风弃光功率 P_shift = sdpvar(T, 1, 'full'); % 可平移负荷调整量 %% 目标函数 Objective = sum(sum(Cg .* P_g), 'all') + sum(C_cur .* P_cur) ... + sum(sum(C_ch .* P_ch), 'all') + sum(sum(C_dis .* P_dis), 'all') ... + C_dr * sum(abs(P_shift)); %% 约束条件 Constraints = []; % 功率平衡:源+储放电+弃风= 负荷+储充电+可平移调整 Constraints = [Constraints, sum(P_g,1) + sum(P_dis,1) + P_re + P_cur ... == P_load + sum(P_ch,1) + P_shift]; % 储能SOC递推 for i = 1:N_s for t = 1:T Constraints = [Constraints, SOC(i,t+1) == SOC(i,t) ... + (eta_ch(i)*P_ch(i,t) - P_dis(i,t)/eta_dis(i)) * dt / E_avail(i)]; end end % SOC上下限,注意考虑SOH后的边界 for i = 1:N_s Constraints = [Constraints, SOC_min(i) <= SOC(i,:) <= SOC_max(i)]; Constraints = [Constraints, SOC(i,T+1) == SOC0(i)]; % 典型日场景才需要的周期末约束 end % 充放电互斥 Constraints = [Constraints, u_ch + u_dis <= 1]; Constraints = [Constraints, 0 <= P_ch <= P_ch_max .* u_ch]; Constraints = [Constraints, 0 <= P_dis <= P_dis_max .* u_dis]; %% 求解与结果提取 ops = sdpsettings('solver', 'gurobi', 'verbose', 2, 'mipgap', 0.001); sol = optimize(Constraints, Objective, ops); if sol.problem == 0 P_g_opt = value(P_g); P_ch_opt = value(P_ch); P_dis_opt = value(P_dis); SOC_opt = value(SOC); else yalmiperror(sol.problem); end这段代码里有几个细节值得重点说。一是P_load和P_re在代码里是外部数据,P_re是风光出力,P_cur表示弃风弃光量,等式左边加上P_cur是为了让"预测出力不能被完全消纳时,多余部分以弃风形式平衡"。二是SOC周期末约束,如果是连续调度场景,不要轻易加这个约束,因为让储能从任意状态回到初始值可能会牺牲经济性;只有研究典型日的时候才需要加。三是充放电互斥约束里,P_ch_max和u_ch之间要广播维度,Matlab里如果P_ch_max是行向量,直接用会报错,我建议初始化时就把P_ch_max定义成列向量,或者用repmat扩展。
3.3 求解器参数设置的几个要点
求解MILP时,mipgap的设置直接影响求解时间和解质量。设0.01(1%的最优性差距)在很多工程场景下够用,设0.0001会很慢。我一般用0.001到0.005之间,先快速拿到可行解,再确认关键指标是否稳定。如果求解时间过长,可以尝试限定TimeLimit,比如120秒,让求解器返回当前最优整数解,虽然不保证全局最优,但对滚动调度基本够用。
还需要注意数值尺度。优化模型里的量纲差容易让求解器数值病态,比如容量是1000MWh,成本是0.5元/kWh,两者差距好几千倍。Yalmip内部一般会做一定的预处理,但如果求解时警告"scaling problems",就要自己统一单位。我的习惯是:功率用MW,容量用MWh,成本用万元/h或元/MWh,所有量纲都在注释里写清楚。这个习惯在调试时能帮你省下大量时间。
4. 多时间尺度协调的实操流程与结果分析
4.1 数据准备:预测数据处理的第一铁律
多时间尺度调度对预测数据的精度要求是分层的。先收集三类数据:负荷预测曲线、风电/光伏出力预测曲线、储能单元状态数据(SOC、SOH、可用容量)。数据颗粒度要与时间尺度匹配:日前用小时级,日内用15分钟级,实时用分钟级。在做代码之前,第一件事是把所有预测数据对齐到同一个时间轴,单位统一,缺失值要用插值补全,而不是让NaN进入优化模型。
这里我想强调一个新手常忽略的点:预测数据的误差不是"平均误差",而是带有时间相关性的。如果直接拿理想预测曲线做日前计划,然后假设日内和实时只是微调,会低估实际运行困难。建议在仿真时对日前预测、日内预测分别叠加不同幅度的误差扰动,测试策略的鲁棒性。这也是论文审稿人最喜欢追问的地方之一。
4.2 滚动时域调度的实现套路
日内滚动调度的实现思路是:在当前时刻k,用未来K个时段的预测数据求解一个有限时域优化问题,只执行第一个时段的决策指令;到下一时刻k+1,更新预测数据,重新求解。这个循环在Matlab里就是一个for循环,但要注意"已执行指令"的固化:已经发生的储能SOC、机组出力、负荷调整量要作为初值传入新模型,不能从头自由优化。
伪代码如下,这是我在实际程序里的写法:
for k = 1:T_total - K % 取当前窗口内的预测数据 P_load_w = load_fcst(k:k+K-1); P_re_w = re_fcst(k:k+K-1); % 更新初始状态 P_g_init = P_g_opt(k); SOC_init = SOC_opt(k); % 调用同一个模型函数 [P_g_w, P_ch_w, P_dis_w] = unit_dispatch_model(P_load_w, P_re_w, SOC_init, ...); % 只记录第一个时段结果 P_g_opt(k+1) = P_g_w(1); SOC_opt(k+1) = SOC_w(1); end注意,如果控制周期是15分钟,而日前计划粒度是1小时,那么日内优化的第一个指令时长也是15分钟,就需要把日前计划和日内计划的变量时间轴做细粒度对齐。这个对齐问题看起来简单,实际最容易出错,因为索引偏移差一个点,整个结果就会错位。我的排查经验是:先用固定数据测试,将滚动调度的结果与日前计划叠加对比,如果曲线在时段边界不连续,大概率是索引错位。
4.3 结果分析:从调度曲线到评价指标
优化跑完后,不要只画几张调度曲线就收工,至少要检查三类内容:功率平衡是否严格满足、SOC是否在安全区间内波动、源储荷三方各自的调节量是否合理。我通常画三张图:第一张是源储荷功率平衡堆叠图,可以直观看到谁在什么时段出力;第二张是各储能单元的SOC曲线,重点看低SOH单元是否被过度使用;第三张是机组出力与日前计划的偏差曲线,反映滚动调度的纠偏效果。
评价指标方面,常用的是运行总成本、弃风弃光率、储能循环次数、负荷峰值削减率。如果要论证"考虑特性分布"的价值,应该做对比实验:将特性分群建模的结果与"等效大电池"建模的结果放在同一算例下对比,看成本上升多少、SOC越限次数降低多少。这种对比实验在论文和项目报告里都很有说服力。
5. 常见问题与排查技巧实录
5.1 求解提示不可行,先查约束还是先查数据
模型不可行是优化调度里最常见的问题。我的排查顺序是:第一步查等式约束的左右两边数值量级,第二步查SOC递推公式里的量纲,第三步查二进制变量是否和连续变量维度匹配。很多时候不可行是因为SOC初始值和上下限矛盾,比如初始SOC低于SOC_min;或者是因为负荷数据里有异常尖峰,超出了所有机组加储能的出力上限。
Yalmip里有个很好用的命令,在求解不可行时可以调用:
diagnostics = optimize(Constraints, Objective, ops); if diagnostics.problem == 1 % 输出冲突约束的检查 checks = check(Constraints); [minviol, loc] = min(checks); % loc对应的约束就是最接近不可行的约束 endcheck(Constraints)返回每个约束的残差,最小值对应的约束就是最可疑的位置。这个功能我几乎每次调试都会用,比盲猜约束省太多时间。
5.2 求解慢、内存爆炸的常见原因
变量规模大是主要原因,尤其是引入了大量二进制变量。优化方向是:第一,减少不必要的二进制变量,比如某储能单元充放电策略固定时,直接固定u_ch和u_dis;第二,减少长时域同时求解的次数,把24小时切成4个6小时区间分别求解,中间用SOC耦合;第三,去掉对结果影响很小的惩罚项和约束。还有一种情况是目标函数里有abs()或max/min函数,Yalmip会引入额外的辅助变量,能用线性重组代替的要尽量代替。
5.3 结果异常:功率不平衡、SOC越限
有时候求解显示成功,但结果明显不物理。功率不平衡最常见的原因是P_re(风光出力)在等式约束里符号搞反了。储能SOC越限则多是因为SOC递推约束没有加上界,或者加的是"终端约束"而不是"全时段约束"。还有一种隐蔽问题:储能效率和容量的单位不一致,比如容量用MWh、功率用MW,Δt用小时没问题,但如果Δt是分钟,转换成小时时漏了除以60,SOC就会漂移。
我把这些整理成一张速查表方便查阅:
| 典型现象 | 可能原因 | 排查建议 |
|---|---|---|
| 模型提示不可行 | 等式约束量级不一致、SOC初值与边界矛盾 | 用check()定位最可疑约束,检查SOC初值是否在上下限内 |
| 求解时间过长 | MILP二进制变量过多、数值尺度差 | 减少二进制变量、限制求解时间、统一量纲 |
| 调度曲线在时段边界不连续 | 滚动窗口索引偏移 | 用固定数据测试,逐点对比计划值 |
| SOC曲线整体漂移 | 效率、容量、时间单位不一致 | 检查SOC递推式中的eta和dt单位 |
| 储能同时充放电 | 未加互斥约束或互斥约束失效 | 检查u_ch+u_dis<=1是否加入约束组 |
| 求解成功但成本异常高 | 惩罚系数设置不合理 | 对比不同惩罚系数量级,参考自然弃风量 |
5.4 我的调试习惯与避坑经验
最后分享几个个人习惯。第一,写一个"傻瓜测试":把储能容量设成非常大、效率设成1.0,看模型是否退化成纯源荷协调问题,结果是否符合直觉。如果这个测试都过不了,说明模型框架有逻辑错误,不要急着调参数。第二,每次改动模型后保留一份旧版本代码,并记录改动前后目标函数的变化。优化问题最容易出现"改了这里好了那里坏"的隐性问题。第三,结果图不要只存final版本,把每次仿真的关键指标(总成本、弃风率、求解时间)写进一个log文件,方便回溯。这几个习惯陪我度过了很多个debug的深夜,也给我省下了大量重复实验的时间。
根据我的项目经验,这类"考虑特性分布的储能电站接入的电网多时间尺度源储荷协调调度策略"的Matlab实现,最容易提升成果质量的地方反而在模型层之外:一是数据颗粒度与时间尺度的匹配是否严谨,二是对比实验是否设计得有说服力。模型和代码只是交通工具,真正让结果可信的是你对物理过程的理解和实验设计的细致程度。