简介:面向需要处理储能系统优化调度问题的Matlab用户,尤其适合风电、光伏等可再生能源并网场景下的研究人员与工程师。内容从工程实际出发,针对储能运行约束给出了三种建模思路:引入一组0-1变量、引入两组0-1变量,以及智能优化算法中的简化表示,并通过一个风光储互补系统的算例对三种方法进行了验证,算例中包含了风电、光伏出力与负荷需求曲线,以及储能的额定容量、功率、充放电效率和荷电状态上下限等关键参数,通过调节储能的充放电策略,可有效降低负荷短缺与弃风弃光现象,有助于读者理解不同建模方式对求解结果的影响。资源包共6个文件,以5个Matlab脚本为核心,涵盖主程序与自定义函数,另附1个PDF方法说明文档,整体压缩包仅669KB,轻量易用。已有2090人学习下载,适合正在学习储能建模或开展智能优化算法研究的开发者参考。 最近在做光储微电网的日前调度,卡在储能运行约束这一块好几天。用Matlab建模时,最烦的不是目标函数怎么写,而是那些功率、SOC、互斥、爬坡约束堆在一起后,要么求解器报无解,要么结果里充电和放电同时为正,跑出来的曲线一看就是错的。这套东西网上的资料大多是零散片段,很少有人把“储能运行约束怎么从物理概念变成Matlab代码”串起来讲。今天把这套建模方法完整梳理一遍,包含我踩过的坑和最终的解决方案。这篇内容适合正在用Matlab做储能调度、微电网优化、电力系统经济调度的同学,尤其是刚接触优化建模、被各种约束搞得头大的朋友。
1. 储能运行约束到底在约束什么
1.1 先想清楚储能的物理边界
任何储能设备本质就是一个“有时间耦合特性的能量容器”。和普通发电机不同,储能除了有功率极限,还要关心它肚子里还剩多少电。这就是为什么储能建模比一般的机组约束麻烦很多。
我在建模时先列了一张参数表,把所有物理量明确出来,后续写代码时几乎就是把这些参数填进去。
| 参数 | 符号 | 典型值 | 说明 |
|---|---|---|---|
| 额定充放电功率 | Pc_max / Pd_max | 500 kW | 功率硬件极限,通常充放对称 |
| 电池容量 | E_max | 2000 kWh | 能存储的最大电量 |
| 初始SOC | SOC_0 | 0.5 | 调度开始时的荷电状态 |
| 充电效率 | η_c | 0.95 | 充电时能量折损 |
| 放电效率 | η_d | 0.95 | 放电时能量折损 |
| SOC下限/上限 | SOC_min / SOC_max | 0.1 / 0.9 | 防止过充过放 |
| 调度时段数 | T | 24 | 通常以1小时为一个时段 |
这里有个容易忽略的点:很多教材里会把充放电效率都记为η,但实际建模时要区分充电侧和放电侧,否则能量平衡会差出一个效率的量级,后面排查起来很痛苦。
1.2 五类约束的物理意义逐个拆解
储能运行约束可以拆成五类,每一类都有明确的物理背景。
功率上下限约束是最基本的:任意一个时刻,充电功率不能超过充电功率上限,放电功率不能超过放电功率上限。这个约束看似简单,但要注意它的变量写法。如果用P_ch和P_dis两个非负变量表示充放电功率,那约束就是0≤P_ch≤Pc_max,0≤P_dis≤Pd_max。但如果用同一个变量P_t允许正负,那约束就麻烦一些,需要引入额外逻辑。
SOC递推约束是储能建模的核心,也是最容易出问题的地方。SOC在相邻时段的递推关系可以写成:
SOC_{t+1} = SOC_t + η_c * P_ch,t / E_max - (1/η_d) * P_dis,t / E_max
这个公式本身不复杂,但有个关键点:充电效率乘在功率上,放电效率要取倒数放在分母位置。这个细节很多人第一次写都会搞反,导致同样的输入功率,SOC变化对不上。
SOC上下限约束实际上是在每一时刻把SOC限制在安全窗口内,防止过充过放。如果你的优化目标是成本最小,那么求解器天然喜欢把SOC推到边界(多用电池减少购电),没有这个约束电池就会直接被“用空”,甚至SOC算出来是负的。
充放电互斥约束解决的是“同一时刻既充电又放电”的悖论。如果不加互斥约束,某些目标函数下,求解器会利用充放电同时进行的漏洞来套利,造成物理上不可能的结果。常见的做法是引入一个二进制变量z_t∈{0,1},用Big-M方法把充电和放电绑定在互斥状态上。
爬坡约束在储能调度中经常被忽略,但对实际电池系统很重要。电池虽然响应快,但功率控制模块在执行指令时会有变化率限制,尤其是大型电化学储能,瞬间从0跳到满功率会对PCS和电池寿命产生不利影响。加了爬坡约束后,调度结果会更贴近实际运行。
首末SOC衔接约束一般用在校核场景。比如微电网每天要保证第二天还能继续调度,调度周期结束时SOC不能太低,否则第二天无电可用。这个约束本质上是为了多日连续运行服务的,单日调度时经常设置末时段SOC等于初始SOC,或者不低于某个下限。
记住这五类,后面无论用什么工具包都能快速对号入座。
2. 把约束写成Matlab能懂的数学语言
2.1 为什么必须引入二进制变量
充放电互斥约束是储能建模中最“有意思”的部分。如果不做任何处理,直接让P_ch和P_dis同时大于0,电动系统的模型是退化的——因为“充电的同时放电”在数学上等效于一个更小的净功率,但能量损耗却按两次计算,实际物理过程根本不存在这个操作。
解决办法是引入二进制变量z_t,表示储能当前的状态:z_t=1表示充电状态,z_t=0表示放电或待机状态。然后加上互斥约束:
P_ch,t ≤ Pc_max * z_t P_dis,t ≤ Pd_max * (1 - z_t)
这个写法就是经典的Big-M约束,Pc_max和Pd_max充当M值。当z_t=1时,第一条约束允许充电功率非零,第二条约束强制放电功率为0;z_t=0时反之。这样一来,物理过程被数学化地表达清楚了。
引入二进制变量后,问题从线性规划(LP)变成了混合整数线性规划(MILP),求解难度会上升,但换来的是约束的准确表达。对于T=24的96点调度,MILP完全在可接受范围内,不需要过度担心性能。
2.2 把约束归整成矩阵形式
Matlab的linprog和intlinprog求解器接受的是标准形约束:Ax≤b,Aeq·x=beq。如果手动把所有约束整理成矩阵,工作量不小,而且容易出错。我的建议是:理解矩阵形式背后的原理,但实际建模时不要手写大矩阵,用循环或者优化工具箱的高层接口来写约束即可。
不过理解矩阵形式仍然有价值。假设调度周期T=3,决策变量按顺序排列为:
x = [P_ch,1, P_ch,2, P_ch,3, P_dis,1, P_dis,2, P_dis,3, z_1, z_2, z_3, SOC_1, SOC_2, SOC_3]
那么功率上限约束可以写成:
对t=1,对应行向量:第1位为1,其它为0,右端项为Pc_max。
SOC递推约束是等式约束,需要放到Aeq·x=beq中,每一行对应一个时段t的递推关系,系数分别是η_c/E_max、-η_d^(-1)/E_max、SOC_t的-1和SOC_{t+1}的1。
当初我手动整理过一次3时段的矩阵,说白了就是帮自己理解每个变量到底去了哪里。真正落地时,我强烈建议用工具箱的约束对象而不是手拼矩阵。
2.3 三套方案选哪一套
Matlab里建储能约束模型常见有三条路:线性规划函数(linprog/intlinprog)、优化工具箱的problem-based接口、以及Yalmip工具箱。我个人的经验是:
| 方案 | 上手难度 | 灵活度 | 适用场景 |
|---|---|---|---|
| 手写矩阵 + intlinprog | 较难 | 最灵活 | 小规模教学演示 |
| problem-based (optimproblem) | 简单 | 中等 | 常规调度建模 |
| Yalmip | 简单 | 高 | 科研、复杂约束、多求解器切换 |
如果你只是交作业或者跑一个简单算例,用problem-based就够了。如果要做研究、对比不同求解器结果、后期扩展到多储能系统,建议直接上Yalmip。它最大的优势是约束写起来和数学公式几乎一一对应,调试时不用在脑海里做“变量下标到矩阵列号”的映射,这个优势在约束多了以后会放大很多倍。
3. 实操过程:从零搭一个储能调度约束模型
3.1 基础数据和决策变量定义
这节用一个具体算例演示怎么落地。场景是24小时调度,储能参数用上面表格里的数值,负荷和光伏数据自己造一组,目标是尽可能降低日购电成本。
% 基础参数 T = 24; % 调度时段数 E_max = 2000; % 电池容量 kWh SOC_min = 0.1; SOC_max = 0.9; SOC_0 = 0.5; eta_c = 0.95; % 充电效率 eta_d = 0.95; % 放电效率 Pc_max = 500; Pd_max = 500; dt = 1; % 时间间隔,单位小时 % 负荷和光伏数据(示例数据,可根据实际情况替换) load_profile = 300 + 200*sin((0:T-1)/24*2*pi) + rand(1,T)*50; pv_profile = max(0, 150*sin((0:T-1)/24*pi));优化变量安排:P_ch(1×T,非负连续变量)、P_dis(1×T,非负连续变量)、z(1×T,二进制变量)、SOC(1×T,连续变量)。一共4T个变量。
% 用problem-based接口创建优化问题 prob = optimproblem('ObjectiveSense','minimize'); % 决策变量 P_ch = optimvar('P_ch',1,T,'LowerBound',0,'UpperBound',Pc_max); P_dis = optimvar('P_dis',1,T,'LowerBound',0,'UpperBound',Pd_max); z = optimvar('z',1,T,'Type','integer','LowerBound',0,'UpperBound',1); SOC = optimvar('SOC',1,T,'LowerBound',SOC_min,'UpperBound',SOC_max);注意,这里的UpperBound已经实现了功率上限约束,所以后面不需要再单独写一组P≤Pmax的不等式约束。很多入门者在这里会重复写约束,虽然不会报错,但会增加冗余计算,没必要。
3.2 核心约束的添加方式
SOC递推约束。这个约束是逐时段写循环完成的:
SOC_prev = SOC_0; eqCons = optimconstr(T,1); for t = 1:T if t == 1 SOC_prev_val = SOC_0; else SOC_prev_val = SOC(t-1); end eqCons(t) = SOC(t) == SOC_prev_val + eta_c*P_ch(t)/E_max - (1/eta_d)*P_dis(t)/E_max; end prob.Constraints.soc_balance = eqCons;这里有个细节:如果用Yalmip,可以先定义SOC(1),然后循环写SOC(t+1)==SOC(t)+...,根本不用处理SOC_0的分支。这也是我喜欢用Yalmip的原因之一,但先用problem-based的方式可以更好地理解底层逻辑。
充放电互斥约束用二进制变量z来绑定:
prob.Constraints.ch_mutex = P_ch <= Pc_max * z; prob.Constraints.dis_mutex = P_dis <= Pd_max * (1 - z);这两组约束把z和充放电功率绑死:z=1时P_dis被强制归零,z=0时P_ch被强制归零。
爬坡约束我选功率变化率限幅方式处理。假设最大爬坡率为Ramp_max=200 kW/h,那么:
ramp_max = 200; rampCons = optimconstr(T-1,2); for t = 1:T-1 % 充电功率的爬坡限制 rampCons(t,1) = P_ch(t+1) - P_ch(t) <= ramp_max; rampCons(t,2) = P_dis(t+1) - P_dis(t) <= ramp_max; end prob.Constraints.ramp = rampCons;如果要做更严格的爬坡约束,还需要把“充电转放电”这类跨越零点的变化率也考虑进去,但那样会引入更多辅助变量,实际工程里一般先按分段功率变化量处理,够用就行。
首末SOC衔接约束,假设末时段SOC要回到0.5:
prob.Constraints.soc_end = SOC(T) == 0.5;3.3 目标函数与求解
目标函数是最小化购电成本。与电网交互功率P_grid = load_profile - pv_profile - P_dis + P_ch,购电成本按分时电价计算:
price = zeros(1,T); price(1:8) = 0.3; % 谷段电价 price(9:18) = 0.9; % 峰段电价 price(19:24) = 0.6; % 平段电价 prob.Objective = sum(price .* (load_profile - pv_profile - P_dis + P_ch) ) * dt;因为P_ch和P_dis是优化变量,P_grid会随它们的取值变化,求解器会在满足所有约束的前提下,自动找到“低电价时充电、高电价时放电”的套利方案。
求解很简单:
options = optimoptions('intlinprog','Display','iter'); [sol, fval, exitflag] = solve(prob, 'Options', options);exitflag=1表示求解成功,如果返回0或-2就需要检查约束是否写错了。
4. 常见问题与排查技巧实录
4.1 模型无解怎么办
这是储能建模最常遇到的坑。第一次跑通之前,基本都会遇到infeasible。排查顺序很关键,我建议按这个顺序来:
第一步,检查SOC边界是否和初始值冲突。比如你设SOC_0=0.5,但SOC_min=0.6,这就直接无解。第二步,检查首末SOC约束。如果你规定SOC(T)=0.3,但整个调度周期的能量收支根本不允许SOC掉到0.3,也会无解。第三步,检查互斥约束的Big-M取值。M值必须大于等于对应功率上限,否则约束可能过紧。第四步,用最简单的目标函数测试——先求“最小SOC变化”之类的平凡目标,逐步添加约束,定位哪一步导致无解。
我当时排查时用了一个笨但极有效的办法:把所有不等式约束的右端项扩大100倍,求解后看哪些约束被严重违反,再逐个收紧。目标是“让模型先有解,再让解合理”。
4.2 产出结果出现微小的同时充放电
这个问题几乎每个人都会遇到。明明加了互斥约束,结果里P_ch和P_dis仍然同时有微小值(比如0.001),原因是intlinprog默认的整数容差(IntegerTolerance)和约束容差允许一定的违反。解决方案有三种:
一是调紧求解器容差:
options = optimoptions('intlinprog','IntegerTolerance',1e-6,... 'ConstraintTolerance',1e-6);二是在约束中人为预留一个小间隙,比如把互斥约束改成P_ch≤Pc_max*z - eps,这在实际工程中不太推荐,因为会引入人为误差。三是后处理:把结果中小于阈值的功率直接置零。这个最实用,也是业界常用做法。我一般设阈值1e-4,处理完再重新校验一次SOC递推是否满足。
4.3 SOC曲线跑飞或者超出边界
如果SOC曲线在某几个时段出现明显的跳跃,先检查效率符号。比如用P_ch=100,η_c=0.95,SOC增量应该是95/E_max,如果算出来是105/E_max,说明你把η_c写反了。还有一个隐蔽问题是单位不统一,功率单位是kW,容量单位是kWh,时间单位是小时,算出来的SOC增量应该无量纲,如果发现维度对不上,先检查这仨。
另外,当T增大到96点(15分钟一个点),SOC递推约束的累计效应会放大微小误差。我遇到过SOC在图表上看没有越界,但小数第四位已经开始偏离物理含义的情况。这时建议在递推约束里把SOC的上限稍微收紧一丢丢,比如UpperBound设置成0.899而不是0.9,给求解器一点容差空间。
4.4 二进制变量过多导致求解变慢
当从单储能扩展到多储能系统时,二进制变量会线性增加,求解时间可能指数级上升。这时除了换更好的求解器(比如Gurobi或Cplex配合Yalmip),还有一个建模技巧值得尝试:把充放电互斥约束用“互补约束+大M法”写成一个约束,而不是分开两行。
具体来说,可以写成:
P_ch + P_dis ≤ Pc_max + Pd_max P_ch ≤ Pc_max * z P_dis ≤ Pd_max * (1 - z)
第一行约束看起来冗余,但它在数值上帮助求解器更快剪枝,对某些大规模问题能节省约30%的求解时间,这是我在一次48储能节点项目中实测出来的结论。
5. 最后分享两个实操技巧
一个是数据管理。做储能调度建模时,我习惯把参数分成两层:物理层参数(额定功率、容量、效率)和调度层参数(初始SOC、边界上下限、电价序列)。物理层参数写进一个结构体里,调度层参数单独设置,这样切换不同工况时不用改动模型主体,只用改结构体里的数值,能省很多时间。
另一个是结果可视化。跑完模型后,我一般会把P_ch、P_dis、SOC画在同一张图上,并在图上标出电价曲线。这个图能一眼看出调度策略是否合理——正常情况应该是谷段充电、峰段放电、SOC曲线跟着电价节奏起伏。如果SOC曲线和电价曲线明显脱节,那大概率是某个约束或参数写错了,优先回去查效率和互斥约束。按照这套方法走下来,储能运行约束的Matlab建模完全可以做到一次成型,不再被无解和异常曲线折磨。
本文还有配套的精品资源,点击获取