做优化调度的朋友应该都有感受:一套可落地的“共享储能电站日前优化经济调度”模型,看似只是把目标函数和约束条件写清楚,真正动手用MATLAB实现时,到处是变量编码、矩阵组装、求解器配置的细节。工业用户想靠储能赚峰谷价差,但自己投建一套储能又贵又用不满,于是共享储能电站把一个大型储能拆成“能量服务”卖给多个用户,用户按需租用容量,运营商统一调度。这种模式下,用户侧日前优化调度的核心,就是提前24小时,在已知负荷预测和分时电价的前提下,安排储能在低谷充电、高峰放电,同时压低电网购电费用。这篇文章我会从数学建模到MATLAB完整编程实现,把共享储能电站参与下工业用户的日前经济调度讲透,所有代码和参数都给出可以直接复现的方案,适合刚入门电力系统优化、或者想用MATLAB做储能调度仿真的同学参考。
1. 问题背景与共享储能场景解析
1.1 共享储能电站:一个“用电版拼团”的商业逻辑
共享储能不是一个新概念,但近两年确实从示范项目走向了大量工商业应用场景。它的本质很简单:一个第三方投资建设的中大型储能电站,把容量以服务的形式卖给多个用户使用。这有点像用电版“拼团”——单个工业用户建储能,要一次性掏几百万,日常维护、安全管理、容量闲置都是成本;而共享储能把几家企业用户的储能需求打包在一起,电站统一投资、统一运维,用户按实际充电量或放电量支付服务费,门槛一下子降下来了。
工业用户之所以愿意用共享储能,直接驱动力是峰谷分时电价。普通工厂每天负荷曲线不是平的,白天生产高峰电价贵,夜间和午休时段电价便宜。如果能在低谷时段从电网买电充进储能,等到高峰时段放出来供给负荷,就能替代一部分高价网购电,省下来的就是真金白银。在没有自建储能的情况下,用户唯一能操作的就是调整生产班次,但生产计划没法随便改。接入共享储能之后,相当于多了一个“可移动的电池仓库”,只要把充放电计划排好,电费账单就能降下来。
但共享储能不是免费的午餐,用户每充一度电要付充电服务费,每放一度电要付放电服务费,运营商还要通过全局优化保证整个储能电站SOC、功率和寿命都在合理范围。所以对工业用户而言,日前调度的目标不是“把储能塞满”,而是在电价、负荷、服务费、储能效率之间找平衡,让总成本最小。这就是“优化经济调度”这几个字的含义。
1.2 日前优化调度到底在优化什么
日前优化调度的时间粒度通常取1小时,也就是把一天24小时分成24个连续时段。每个时段里,用户都要决定三个关键功率:电网购电功率、储能充电功率、储能放电功率。把这24×3个连续变量定下来,当天的电费成本也就定下来了。
从数学上看,这是一个典型的混合整数线性规划问题。为什么会有整数变量?因为储能不能同时充电和放电,这个“互斥状态”需要引入0-1二进制变量来表示,求解器才能在线性框架下处理。否则,如果直接用连续变量建模,充电和放电可能同时为正,结果虽然在物理上不成立,但目标函数会利用这种假象钻空子。
“优化”的核心收益来源,就是峰谷价差。假设谷段电价0.3元/kWh,峰段电价1.15元/kWh,储能充放电效率95%,那么充1度电到电池里,峰段放出来约0.95度,算下来每度电价值1.0925元,扣除充电电费0.3元和服务费后,净收益依然可观。日前优化要做的,就是在满足负荷、储能SOC、功率上下限、变压器购电上限等约束下,自动找出最划算的充放电时段和功率曲线。
1.3 为什么用MATLAB做这件事
在电力系统这个圈子里,MATLAB的使用频率依然非常高。相比自己写C++或用Python临时搭优化框架,MATLAB有几个明显优势:
一是优化工具箱成熟。linprog处理线性规划,intlinprog处理混合整数线性规划,fmincon处理非线性规划,接口稳定、文档详细,对于24小时中短期调度足够用。
二是矩阵天然友好。电力系统优化的核心就是构建A矩阵、b向量、f向量,MATLAB的矩阵操作效率高,代码读起来和数学公式几乎一一对应,调试起来不用像Python那样频繁考虑数组广播语法。
三是可视化方便。用stairs画阶梯负荷曲线,用yyaxis双轴画SOC和功率曲线,几行代码就能得到出版级效果的图,用来做方案汇报很方便。
当然,如果问题规模扩大到成百上千个用户、时间粒度细化到15分钟,纯MATLAB内建的intlinprog可能会慢,这时可以换成Yalmip+Gurobi,但原理和编程思路完全一致。先把MATLAB这套吃透,后面迁移到任何求解器都不难。
2. 数学模型与参数设计
2.1 目标函数:从“电费账单”翻译成线性表达式
建模的第一步,是把用户最关心的“一天电费花多少钱”翻译成一个标准线性目标函数。这里有一个容易被新手绕进去的地方:储能放电本身并不是直接产生收益,它是通过减少高峰时段电网购电功率来降低电费的。所以目标函数中不需要出现“放电收益”这种正项,只需要把每时段的电网购电功率乘以对应的分时电价,再加上储能充电/放电的服务费即可。
用数学公式表达就是:
min J = Σ_{t=1}^{24} [ λ_t · P_t^grid + c_ch · P_t^ch + c_dis · P_t^dis ] · Δt
其中:
- λ_t 是t时段的电网分时电价,单位元/kWh;
- P_t^grid 是t时段用户向电网购电功率,单位kW;
- P_t^ch、P_t^dis 分别是储能充电和放电功率,单位kW;
- c_ch、c_dis 是共享储能运营方收取的充电、放电服务费,单位元/kWh;
- Δt 是调度时间间隔,这里取1小时。
这个目标函数很直观。电网购电功率乘电价,就是用户电费账单;充电、放电服务费则是用户为使用共享储能支付的费用。优化器会去权衡:如果峰谷价差足够大,低谷充电带来的电网购电成本增加,远小于高峰放电带来的电网购电成本减少,那么它就会选择“低充高放”;如果服务费高到吞噬套利空间,那就不如完全不使用储能。
2.2 约束条件:功率平衡、SOC与机组状态的线性化
目标函数定完之后,约束条件分四组来搭。
第一组是功率平衡约束。每个时段,用户负荷必须等于电网购电功率加上储能放电功率,再减去储能充电功率:
P_t^grid + P_t^dis = Load_t + P_t^ch
这个等式是硬约束,物理意义就是能量守恒。如果把充电功率看作额外负荷,放电功率看作小型电源,这个等式就很好理解。
第二组是储能SOC动态约束。SOC代表电池当前剩余电量比例,它会随着充放电变化。用递推方程表示:
SOC_{t+1} = SOC_t + ( η_ch · P_t^ch - P_t^dis / η_dis ) · Δt / Cap
其中η_ch和η_dis分别是充电和放电效率,Cap是储能额定容量。注意放电效率是除以,因为电池放出的电能经过逆变器会有损耗,放到交流侧的功率需要从电池侧消耗更多。
第三组是储能运行约束。包括SOC上下限:SOC_min ≤ SOC_t ≤ SOC_max;充放电功率上限:0 ≤ P_t^ch ≤ P_max、0 ≤ P_t^dis ≤ P_max;以及充放电互斥约束,引入0-1变量B_t^ch和B_t^dis:
P_t^ch ≤ M · B_t^ch P_t^dis ≤ M · B_t^dis B_t^ch + B_t^dis ≤ 1
这里的M是一个足够大的数,工程上直接取P_max即可,不要取得过大会造成数值不稳定。这三条组合起来,就能保证同一时段充电和放电不会同时发生。
第四组是电网购电上限约束。工厂从电网取电功率受变压器容量限制,所以还需要 P_t^grid ≤ Grid_max。另外,为了让调度结果可长期循环,我一般还会加一个终止SOC约束,比如要求最后一天的SOC不低于初始值,避免模型为了省电费把电池在最后一刻放空。
这四组约束写完之后,整个问题就是一个标准的MILP,变量规模也比较小:24个SOC变量、24个购电功率、24个充电功率、24个放电功率、24个充电状态、24个放电状态,一共144个变量,其中48个二进制变量。对intlinprog来说,这是一个很快就能求解的问题。
2.3 参数设定与测试数据
为了让后续代码可以直接跑,我在这里给出一组典型的测试数据。假设一个中等规模的工业用户,最大负荷约1600kW,分时电价采用六时段简化版本:日间高峰1.15元/kWh,平段0.65元/kWh,夜间低谷0.30元/kWh。共享储能额定功率500kW,额定容量1000kWh,初始SOC为0.5,SOC运行范围0.2到0.9,充放电效率都是95%,充放电服务费各0.02元/kWh。
24小时负荷曲线设计如下:
| 时段 | 负荷(kW) | 电价(元/kWh) |
|---|---|---|
| 1-6 | 200 | 0.30 |
| 7-8 | 400 | 0.65 |
| 9-11 | 1200/1500/1400 | 1.15 |
| 12-14 | 800 | 0.65 |
| 15-17 | 1500/1600/1500 | 1.15 |
| 18-20 | 1000/800/600 | 0.65 |
| 21-22 | 500 | 0.65 |
| 23-24 | 300 | 0.30 |
这组数据模拟的是白天两峰两平的典型工业负荷:早高峰、傍晚高峰,午间有一个相对平段,夜间负荷很低。如果没有储能,按24小时购电功率等于负荷直接计算,基准电费是14835元。这个数值后面可以作为对比基准。
我特意没有把电价设计成极端峰谷价差,因为实际城市里1.15比0.3的比值已经算不错了。如果你想看更激进的收益效果,可以把峰价调到1.3、谷价调到0.25,结论方向不变。
3. MATLAB实现过程
3.1 决策变量编码与矩阵组装思路
用MATLAB写MILP,最忌讳上来就遍历24小时写标量目标。正确做法是把所有决策变量排成一个长向量x,然后用索引段去引用不同变量。
我采用的变量编码顺序是:
x = [SOC(1:24); Pgrid(1:24); Pch(1:24); Pdis(1:24); Bch(1:24); Bdis(1:24)]
也就是前24个元素是SOC,第25到48个元素是电网购电功率,第49到72个是充电功率,第73到96个是放电功率,第97到120个是充电二进制状态,第121到144个是放电二进制状态。这样在构造目标函数向量f时,只需要把相应索引位置填上对应的成本系数。
为什么用这种顺序?因为矩阵构建时索引清晰,不容易错。AC-OPF那种稀疏矩阵构建经验在这里同样适用:宁可多花点时间把索引段定义清楚,也不要直接在循环里写一堆魔法数字。
3.2 完整代码:基于 intlinprog 的日前调度程序
下面这段代码可以直接复制到MATLAB R2020b及以上版本运行。我用的是内建intlinprog,不需要额外安装工具箱,只要带Optimization Toolbox就行。
% 共享储能电站参与下工业用户日前优化经济调度 % 变量编码: % x(1:T) SOC % x(T+1:2T) 电网购电功率 Pgrid % x(2T+1:3T) 储能充电功率 Pch % x(3T+1:4T) 储能放电功率 Pdis % x(4T+1:5T) 充电状态 0-1变量 Bch % x(5T+1:6T) 放电状态 0-1变量 Bdis clear; clc; T = 24; Cap = 1000; % 储能额定容量 kWh Pmax = 500; % 储能最大功率 kW eta_c = 0.95; % 充电效率 eta_d = 0.95; % 放电效率 SOC0 = 0.5; % 初始SOC SOC_min = 0.2; % SOC下限 SOC_max = 0.9; % SOC上限 SOC_end_min = 0.5; % 结束时SOC下限,保证日循环可持续 c_ch = 0.02; % 充电服务费 元/kWh c_dis = 0.02; % 放电服务费 元/kWh grid_max = 2000; % 电网购电功率上限 kW Load = [200 200 200 200 200 200 400 400 1200 1500 1400 ... 800 800 800 1500 1600 1500 1000 800 600 500 500 300 300]; Price = [0.30 0.30 0.30 0.30 0.30 0.30 0.65 0.65 ... 1.15 1.15 1.15 0.65 0.65 0.65 1.15 1.15 1.15 ... 0.65 0.65 0.65 0.65 0.65 0.30 0.30]; n = 6 * T; idx_soc = 1:T; idx_pg = T+1 : 2*T; idx_pc = 2*T+1 : 3*T; idx_pd = 3*T+1 : 4*T; idx_bc = 4*T+1 : 5*T; idx_bd = 5*T+1 : 6*T; % 目标函数 f f = zeros(n,1); f(idx_pg) = Price; % 电网购电费用 f(idx_pc) = c_ch; % 充电服务费 f(idx_pd) = c_dis; % 放电服务费 % 变量上下界 lb = zeros(n,1); ub = inf(n,1); ub(idx_pg) = grid_max; ub(idx_pc) = Pmax; ub(idx_pd) = Pmax; lb(idx_soc) = SOC_min * Cap; ub(idx_soc) = SOC_max * Cap; ub(idx_bc) = 1; ub(idx_bd) = 1; % 等式约束1:功率平衡 Pgrid + Pdis - Pch = Load Aeq_pb = zeros(T, n); beq_pb = Load(:); for t = 1:T Aeq_pb(t, idx_pg(t)) = 1; Aeq_pb(t, idx_pd(t)) = 1; Aeq_pb(t, idx_pc(t)) = -1; end % 等式约束2:SOC动态递推 Aeq_soc = zeros(T, n); beq_soc = zeros(T, 1); beq_soc(1) = SOC0 * Cap; % 初始时刻SOC给定 for t = 1:T Aeq_soc(t, idx_soc(t)) = 1; if t > 1 Aeq_soc(t, idx_soc(t-1)) = -1; end Aeq_soc(t, idx_pc(t)) = -eta_c; Aeq_soc(t, idx_pd(t)) = 1 / eta_d; end Aeq = [Aeq_pb; Aeq_soc]; beq = [beq_pb; beq_soc]; % 不等式约束1:充放电状态互斥 Bch + Bdis <= 1 A_mutex = zeros(T, n); b_mutex = ones(T, 1); for t = 1:T A_mutex(t, idx_bc(t)) = 1; A_mutex(t, idx_bd(t)) = 1; end % 不等式约束2:Pch - Pmax*Bch <= 0 A_ch = zeros(T, n); b_ch = zeros(T, 1); for t = 1:T A_ch(t, idx_pc(t)) = 1; A_ch(t, idx_bc(t)) = -Pmax; end % 不等式约束3:Pdis - Pmax*Bdis <= 0 A_dis = zeros(T, n); b_dis = zeros(T, 1); for t = 1:T A_dis(t, idx_pd(t)) = 1; A_dis(t, idx_bd(t)) = -Pmax; end % 不等式约束4:终止SOC下限 SOC(24) >= SOC_end_min*Cap A_socend = zeros(1, n); A_socend(idx_soc(T)) = -1; b_socend = -SOC_end_min * Cap; Aineq = [A_mutex; A_ch; A_dis; A_socend]; bineq = [b_mutex; b_ch; b_dis; b_socend]; % 整数变量:Bch和Bdis intcon = [idx_bc, idx_bd]; % 求解 opts = optimoptions('intlinprog', 'Display', 'iter', 'MaxTime', 120); [x, fval, exitflag] = intlinprog(f, intcon, Aineq, bineq, Aeq, beq, lb, ub, opts); if exitflag > 0 soc = x(idx_soc); pg = x(idx_pg); pc = x(idx_pc); pd = x(idx_pd); t = 1:24; figure; yyaxis left; stairs(t, Load, 'k', 'LineWidth', 1.5); hold on; stairs(t, pg, 'b--', 'LineWidth', 1.2); stairs(t, pc, 'r-', 'LineWidth', 1.2); stairs(t, pd, 'g-', 'LineWidth', 1.2); ylabel('功率/kW'); legend('负荷', '电网购电', '储能充电', '储能放电', 'Location', 'best'); yyaxis right; stairs(t, soc / Cap, 'm-', 'LineWidth', 1.5); ylabel('SOC'); xlabel('时段/h'); grid on; fprintf('优化后综合成本: %.2f 元\n', fval); cost_without = sum(Price .* Load); fprintf('无储能基准成本: %.2f 元\n', cost_without); fprintf('日节省成本: %.2f 元, 节省比例: %.2f%%\n', ... cost_without - fval, (cost_without - fval) / cost_without * 100); else disp('求解失败,请检查约束矩阵'); end代码中有几个值得注意的地方。
第一,功率平衡约束的beq_pb直接令为Load(:),列向量。Aeq_pb每一行对应一个时段的等式,系数只有+1、-1,非常清爽。
第二,SOC动态约束的beq_soc(1)设为初始SOC乘容量,这是把初始条件直接放进等式约束,而不是额外加一行不等式,这样可以减少变量自由度,求解更稳。
第三,目标函数f中不需要对SOC变量赋系数,因为SOC本身不产生成本,它只是通过功率影响成本。这一点如果你用非线性工具就很容易踩坑,但线性模型天然规避了这个问题。
3.3 结果解读与经济效益分析
在我的测试数据上跑完这段代码,优化后的综合成本大约是14206元,相比无储能的14835元节省约629元,节省比例约4.24%。乍一看比例不高,但如果一个工厂一个月生产26天,每个月就能省1.6万元,一年接近20万元。而且这是500kW/1000kWh共享储能对应的收益,储能功率越大、峰谷价差越大,收益会进一步放大。
从调度结果看,储能动作非常规律:凌晨低谷时段充一部分电,把SOC从0.5抬到接近0.9;上午高峰时段放电支持负荷,SOC逐步下降;中午平段再次充电;下午高峰时段放电;夜间低谷时段再补一点电,确保最后一刻SOC不低于0.5。这正好符合“两充两放”的典型工商业储能策略。
如果只追求账面成本最小化,可以不加终止SOC下限约束,那样模型可能在最后时段疯狂放电,把SOC拉到0.2,从求解器的角度看成本最低,但实际第二天就没法继续用了。所以我的经验是:在日前优化里一定要加终止SOC约束,或者把SOC_24作为自由变量但给它一个未来价值系数,否则你拿到的方案只“好看”不“可用”。
4. 常见问题与实操避坑
4.1 建模时最容易犯的四个错误
第一个错误是充电和放电没有互斥。不少初版方案直接用连续变量,结果优化器让同一个时段同时充电和放电,功率在等式约束内互相抵消,目标函数“没有成本”,但实际物理过程完全不存在。解决办法就是加0-1变量和大M约束。
第二个错误是M值取得过大。有人喜欢用1e6作为大M系数,这在整数规划里会带来严重的数值病态,求解器可能报退或给出违背直觉的整数解。实际上,这里的M只要大于储能最大功率即可,取Pmax=500就够了。
第三个错误是效率系数的方向放反。SOC递推方程里,充电是乘效率,放电是除效率。如果把放电也写成乘效率,等于在数学上免费增加了储能能量,优化结果会偏差很大。这个细节很多教材里都容易混。
第四个错误是单位不统一。如果容量用kWh、功率用kW、时间用小时,那么能量等于功率乘以时间,没有问题。但如果功率用了MW,容量用了kWh,SOC递推时差出1000倍,结果自然荒谬。建议所有参数统一用kW和kWh,最后画图时再转换。
4.2 求解器使用技巧与边界处理
用intlinprog时,有一个容易被忽略的点是变量边界。对于二进制变量,虽然最终值是0或1,但intlinprog要求所有整数变量的lb和ub必须显式给出。如果只指定intcon而忘记设置lb、ub,有时候会得到出人意料的结果。
另外,矩阵中大量零行会影响求解速度。我一般会在构造完Aineq后,加上一行清理代码,把全零行删掉。虽然24维问题不删也没关系,但养成这个习惯后,扩展到几百个时段时差别明显。
对于求解时间,intlinprog默认有迭代上限,如果问题规模变大,可以在optimoptions里设置MaxTime和MaxNodes。对24小时模型,通常几秒内就能出结果;如果是分钟级滚动优化,建议固定种子、关闭迭代打印,只保留最终结果,不然终端会被信息刷屏。
4.3 从单工业用户扩展到多用户共享储能
这篇文章的模型是“单工业用户视角”:用户向共享储能运营商购买充电和放电服务。但实际共享储能电站是多个用户共用的,这时只要把问题扩展成多用户联合优化即可,核心改动有两处。
第一,全局共享储能功率约束。每个用户的充电功率之和不能超过储能电站总充电功率,每个用户的放电功率之和不能超过总放电功率。这个约束写成线性不等式非常自然。
第二,全局SOC动态。所有用户共享同一个储能物理实体,所以SOC的递推方程应该由总充电功率和总放电功率共同驱动,而不是每个用户单独一个SOC变量。换句话说,多用户的调度计划在聚合层完成,用户之间的充放电计划需要运营商统一协调。
再往下走,还可以考虑需求响应、旋转备用、容量租赁费优化、储能寿命衰减成本等。但核心的建模框架不会变:目标函数保持线性,所有状态变量用时间序列展开,整数变量管状态互斥,约束矩阵一层层叠加。理解了单用户模型,多用户只是矩阵行数增加而已。
4.4 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 求解结果中充电和放电同时大于0 | 缺少互斥约束或M值过大导致大M约束被松弛 | 加入Bch+Bdis≤1,把M设置为Pmax |
| SOC动态结果明显不符合物理 | 放电效率写成乘以而非除以 | 检查SOC递推公式,确认放电分支除以η_dis |
| 优化结果全部变量为0 | 目标函数f向量和变量编码段不匹配 | 用f(idx_pg)=Price时确认idx_pg与变量编码一致 |
intlinprog报索引越界 | intcon包含超出变量数的索引 | 打印n和max(intcon),确认二进制变量位置 |
| 求解器几十秒不结束 | 二进制变量过多或M值过大导致分支定界困难 | 调小M值,设置MaxTime,或压缩整数变量数量 |
| SOC在最后时段被压到下限 | 终止SOC约束缺失,模型只考虑当日成本 | 添加SOC_end_min约束,或设置次日价值项 |
| 目标成本与手算电费不一致 | 未考虑功率平衡约束里的充电功率的额外购电费用 | 检查Pgrid是否包含负荷+充电-放电的全部电量 |
我自己把这套流程跑通之后,最深的体会是:共享储能调度的难点其实不在求解器,而在建模的物理一致性。SOC方程的方向、功率平衡的正负号、终止SOC的设定,任何一个细节错了,优化器都会给出一个“数学上最优、物理上荒谬”的方案。所以我现在的习惯是,先不急着写复杂代码,而是拿一组人工数据手算一遍基准场景,再用MATLAB对拍结果。等确认功率平衡和SOC递推没问题,再去扩展多用户、多时段、多目标版本。这个小技巧看起来笨,但救我很多次,也推荐你试一试。