考虑储能特性分布的多时间尺度源储荷协调调度Matlab实现
2026/9/9 6:47:22 网站建设 项目流程

1. 从标题到代码:这个项目到底在解决什么问题

写这篇分享之前,我刚好在办公室跑完一版基于Matlab的储能调度仿真。项目名字很长:“考虑特性分布的储能电站接入的电网多时间尺度源储荷协调调度策略(Matlab代码实现)”。念起来有些拗口,但落地到代码里其实就是一件事:当储能电站不是一台“大电池”,而是几十个甚至上百个特性不一的储能单元时,电网怎么在不同时间尺度上把源、储、荷安排明白。这篇文章按我的实际复现思路来写,适合正在做电力系统优化调度、储能参与电网运行,或者刚拿到类似题目还没想清楚从哪下手的同学参考。

1.1 拆开标题:储能电站不是一台“大电池”

“特性分布”这四个字,是整个题目的题眼。很多课程设计或论文里,储能电站常常被简化成一个聚合模型:给定额定功率和额定容量,SOC按一个统一状态量更新。这种做法做宏观分析没问题,但真实工程里,储能电站一般由多个电池簇、变流器并联组成,各电池簇出厂批次不同、运行温度不同、充放电深度不同,导致SOC、SOH、可充放电功率都存在明显差异。如果调度指令只下发到一个聚合储能,执行层再盲目平均分配功率,很容易出现部分单元过充过放、另一部分单元却闲置的情况,久而久之,整站可用容量和寿命都会明显下降。

我举个很直观的例子。假设一个储能电站有两台电池簇,1号容量2MWh、最大功率1MW,2号容量0.5MWh、最大功率0.25MW。如果调度模型把它们聚合成“总容量2.5MWh、总功率1.25MW”的一台储能,系统优化后给出的计划可能是“以0.6MW充电一小时”。这个功率单独看没问题,执行层如果按额定功率比例分配给两个单元,1号分到0.48MW,2号分到0.12MW,两个单元都正常。但如果执行层按平均功率0.3MW去分配,2号就会直接超过额定功率,SOC也会迅速越限。现实中的分配算法当然不会这么粗糙,但聚合模型本身会掩盖很多类似的执行风险。所以在Matlab代码实现里,这一步通常体现为:把原来单个储能变量改成多个储能单元变量,每个单元有自己的SOC初值、容量上限、功率上限、健康系数,甚至在目标函数里加入各单元SOC一致性惩罚项。模型维度变大,求解难度上升,但换来的结果是调度指令更可靠,储能电站内部不会出现“旱的旱死、涝的涝死”。

1.2 场景定位:多时间尺度源储荷协调在做什么

储能接入电网后,可以参与调峰、调频、备用、缓解阻塞等多种服务。但这些服务的时间常数差别很大:调峰是小时到天级别的能量搬运,调频是秒到分钟级别的功率跟踪,阻塞管理和电压支撑则需要在更短的时间窗口内响应。任何单一时间尺度的模型都没法同时把这些事情说清楚。这就是“多时间尺度”的来源。

我在复现中采用了常见的三层结构:日前调度计划以1小时为分辨率,决定未来24小时传统机组启停、机组出力、联络线功率和储能各单元充放电计划,时间尺度最长,主要解决经济性和电力平衡问题;日内滚动优化以15分钟为周期,基于超短期负荷预测和新能源预测,修正机组出力与储能计划,重点跟踪新能源波动和负荷预测偏差;实时控制层以1分钟甚至更短为周期,只做小幅调整和偏差平抑,负责把调度指令落实到实际功率。三层之间不是各干各的,而是通过储能SOC目标值、机组出力基点、联络线计划值层层传递。

源储荷协调则强调不能只看火电和储能。新能源出力波动大的时候,负荷侧也需要配合,比如可中断负荷、可转移负荷、电动汽车充电桩等柔性资源,在高峰时段削减用电或在低谷时段增加用电,给储能和机组减轻压力。整篇代码最终输出的是一个包含传统机组出力、新能源消纳功率、储能充放电功率、负荷调整量的综合运行方案,而不是孤立的储能充放电曲线。我在写代码时,把变量分成了“源侧变量”、“储能侧变量”、“负荷侧变量”三个模块,这样后续加新资源、新约束都会方便很多。

2. 方案选型与建模思路:为什么这样做

2.1 储能特性分布怎么进优化模型

储能电站内部特性分布,建模时有两种层次。

第一种是把每个储能单元作为独立优化变量,约束里分别写SOC动态、充放电功率上下限,目标函数里加上各单元之间SOC偏离基准值的惩罚项。比如第i个储能单元在t时刻的SOC记为soc(i,t),充电功率pch(i,t),放电功率pdis(i,t),动态约束写成soc(i,t+1)=soc(i,t)+eta_ch*pch(i,t)/E(i)-pdis(i,t)/(E(i)*eta_dis),同时pch(i,t)和pdis(i,t)不能同时大于0,并受额定功率限制。这样储能电站的总功率就是所有单元功率之和。

第二种是用“分布式特性聚合”的方式,把储能单元按SOC区间或SOH档位聚合成若干组,每组特性内部一致,再把组作为优化变量。这种方法可以控制变量数目,适合储能单元特别多的大规模电站。我在Matlab仿真里,为了同时兼顾真实性和求解速度,取了12个储能单元,特性参数按正态分布随机生成——初始SOC分布在0.2到0.8之间,额定容量在0.5到1.5倍基准值之间,健康系数在0.85到1.0之间。这样既能看到特性差异对调度结果的影响,又不会让模型膨胀到没法在普通笔记本上求解。

选型时还要考虑:特性分布到底该放进目标函数还是约束里?我的做法是两边都放。约束保证每个储能单元都不能越限,这是硬约束;目标函数里加入SOC均衡惩罚,鼓励各个单元在运行过程中SOC保持接近,这是软约束。软约束的作用非常大,没有它,求解器只会在满足约束的前提下把某个单元往死里用,其他单元闲着,一周下来就会发现部分单元频繁满充满放,寿命衰减明显。

提示:如果做论文复现,尽量不要把特性分布写死成固定数组。用随机生成并固定随机种子,结果可重复,又方便做多场景对比。

2.2 多时间尺度协调调度的整体框架

三层优化在Matlab里通常不是一次跑完,而是用一个时序循环嵌套实现。我把整体框架设计成三层循环加一个状态更新函数:

  • 最外层是日前调度,1小时一个段,跑24段;
  • 中间层是日内滚动,每15分钟一个点,每个日前段内做96个点的滚动修正;
  • 最内层是实时控制,以1分钟为间隔,模拟系统在日内计划基础上对分钟级扰动做出的响应。

实际仿真里,并不需要把三层都做得特别复杂。我复现时把重点放在日前和日内两层,实时控制层做了简化,用一个比例调节器模拟储能快速响应。这样既满足“多时间尺度”的主题,又不会因为模型过于复杂导致代码调试周期无限拉长。

下表是我在代码中使用的各时间尺度配置:

时间尺度分辨率优化周期主要决策变量更新触发条件
日前调度1小时24小时机组启停、机组出力、储能日前计划、可中断负荷计划每天开始前
日内滚动15分钟4小时(向前滚动)机组出力修正、储能功率修正、新能源弃用调整每个15分钟开始时
实时控制1分钟1分钟储能快速功率补偿、备用响应每个采样点

层与层之间传递的核心状态是储能SOC。日前调度结束时会给每个储能单元一个24小时后的目标SOC,日内滚动优化会尽量追踪日前SOC曲线,实时控制则更新最新SOC,并把当前SOC作为下一轮滚动优化的初始条件。我在代码里用了一个结构体state_storage.soc来存所有储能单元的SOC向量,每一层在做完计算后都调用同一个update_soc()函数更新。如果哪一层忘了更新SOC,后面所有结果都会偏,这个问题后面会详细说。

2.3 目标函数与约束构建细节

目标函数不是越复杂越好,但要能体现调度的经济性和安全性。我的代码中采用加权求和的形式:

第一项是传统机组运行成本,用二次函数表示煤耗成本,必要时还要加上启停成本;第二项是弃风弃光惩罚,目的是让调度结果尽量消纳新能源;第三项是储能单元SOC均衡惩罚,计算公式是各单元SOC与全站平均SOC差值的平方和乘一个权重;第四项是负荷调整惩罚,因为可中断负荷不能随便切,切得越多成本越高。

约束方面,我按类型分了四组:

  1. 系统级约束:任意时刻电源总出力加上储能放电功率、减去储能充电功率,等于负荷功率减去实际削减的可中断负荷。这是必须满足的等式约束,也是最容易导致模型不可行的约束。
  2. 机组约束:每台机组的出力上下限、爬坡速率限制、最小启停时间限制。在日前模型中,启停是0/1整数变量;在日内滚动模型中,我把启停状态固定为日前结果,只调整连续出力变量,这是为了降低求解难度。
  3. 储能约束:每个储能单元的SOC上下限、充放电功率上下限、充放电互斥、SOC动态更新,以及各单元SOC尽量接近的软约束。另外还要考虑同一储能电站内总功率与外部交互的功率限制。
  4. 网络约束:如果用的是IEEE节点系统,还需要考虑直流潮流或线性化交流潮流约束,比如线路有功潮流不超过上限、节点电压偏差在允许范围内。我的算例用了修改后的IEEE 33节点配电网,所以加入了简化的DistFlow线性潮流约束。

数学形式在论文里可以写得很严谨,但代码实现时我建议先做“无网络约束”的版本,把功率平衡和储能约束跑通,再加潮流约束。一步到位很容易出现一堆infeasible,排查起来非常头大。尤其是潮流约束里的节点电压幅值,如果写成非凸的非线性形式,Matlab求解器根本没法直接处理,必须线性化或者用锥规划近似。这也是我在做配电网模型时踩过的坑,后面会单独说。

3. Matlab代码实现的核心环节

3.1 环境准备:YALMIP + Gurobi 这套组合

Matlab里做优化调度,最舒服的还是YALMIP加外部求解器。YALMIP是一个建模层,能帮你把优化变量、约束和目标函数组织成标准形式,再调用底层求解器。Gurobi在求解混合整数线性规划方面性能很好,学术许可申请也方便。如果机器上没有Gurobi,可以先用Matlab自带的intlinprog跑小算例,但一旦储能单元数量超过20个、整数变量超过100个,intlinprog的速度会让你怀疑人生。

安装时需要注意,YALMIP不是MATLAB自带工具箱。下载后把压缩包解压到任意目录,在MATLAB里addpath(genpath('你的YALMIP目录')),然后savepath,防止下次重启失效。Gurobi安装完后,要在MATLAB里运行gurobi_setup配置接口。我第一次配置时遇到一个常见问题:64位Gurobi与32位MATLAB不匹配,会直接报Undefined function 'gurobi_mex'。解决方法很简单,换用64位MATLAB。

如果只是想复现调度逻辑,不想折腾商业求解器,可以用YALMIP的默认求解器Sedumi或SDPT3,但MILP问题还是建议装Gurobi。我实测下来,同样一个包含12个储能单元、24小时日前调度模型,Gurobi用十几秒就能找到最优解,而intlinprog可能需要几分钟甚至更久。另外,MATLAB R2021a以后自带的optimproblem框架也能写线性规划,但处理混合整数变量和复杂高阶约束时,还是YALMIP更灵活。要不要装Gurobi,我的建议是:只要你的项目不是只跑一次,就装。磨刀不误砍柴工。

3.2 数据准备与参数初始化

代码的第一步是准备数据。我把所有输入数据放在一个结构体data里,这样后续函数传参方便,也容易做批处理。典型字段包括:

  • data.load_forecast:未来24小时负荷预测,维度24x1;
  • data.wind_forecastdata.pv_forecast:新能源出力预测,维度24x1;
  • data.gen:传统机组的各项参数,包括额定出力上下限、爬坡速率、煤耗成本系数;
  • data.storage:储能电站参数,包含单元数n_storage,每个单元的额定容量、额定功率、SOC初值、健康系数、充放电效率;
  • data.price:分时电价。

关键是储能参数怎么生成。我在初始化时用了固定随机种子:

rng(2024); n = 12; data.storage.E = 1.0 + 0.5 * rand(n,1); % 额定容量,MWh data.storage.P = 0.5 + 0.2 * rand(n,1); % 额定功率,MW data.storage.soc0 = 0.2 + 0.6 * rand(n,1); % 初始SOC data.storage.eta_ch = 0.95; data.storage.eta_dis = 0.95; data.storage.health = 0.85 + 0.15 * rand(n,1); % 健康系数

这样做的好处是:每次运行结果一致,但各储能单元特性明显不一样,正好体现“特性分布”这个主题。如果你希望验证不同分布的影响,可以改随机种子或换用makedist生成特定分布的数据。

参数初始化里最容易忽略的是基准值设置。调度模型里很多量级相差很大,比如负荷功率几百兆瓦,SOC只有0到1,目标函数里煤耗成本系数几百元/兆瓦时。如果直接混在一起,求解器数值条件会很差。我的习惯是把所有功率和容量先标幺到100MVA基准,或者至少统一到同一量纲。SOC保持0-1,但目标函数里的SOC均衡惩罚项要乘一个合适的权重,比如1元/单元单位偏差,不然会被煤耗成本淹掉。

3.3 日前调度模块实现

日前调度模块是整段代码的核心。我用YALMIP建模,关键代码如下:

% 日前调度模型 Nt = 24; Pg = sdpvar(Ng, Nt); % 机组出力 u = binvar(Ng, Nt); % 机组启停状态 Pch = sdpvar(Ns, Nt); % 储能充电功率 Pdis = sdpvar(Ns, Nt); % 储能放电功率 soc = sdpvar(Ns, Nt+1); % 储能SOC Pcut = sdpvar(Nt, 1); % 弃风弃光功率 Pload_shed = sdpvar(Nt, 1); % 可中断负荷削减 Constraints = []; % 系统功率平衡 for t = 1:Nt Constraints = [Constraints, ... sum(Pg(:,t)) + sum(Pdis(:,t)) + data.wind_forecast(t) - Pcut(t) ... == data.load_forecast(t) + sum(Pch(:,t)) - Pload_shed(t)]; end % 机组约束、储能约束省略... % 储能SOC递推 for i = 1:Ns for t = 1:Nt Constraints = [Constraints, ... soc(i,t+1) == soc(i,t) + data.storage.eta_ch * Pch(i,t) / data.storage.E(i) ... - Pdis(i,t) / (data.storage.E(i) * data.storage.eta_dis)]; end end Objective = sum(sum(gen_cost_coeff .* Pg)) + ... wind_penalty * sum(Pcut) + ... shed_penalty * sum(Pload_shed) + ... soc_balance_penalty * sum(sum((soc - mean(soc,1)).^2)); ops = sdpsettings('solver','gurobi','verbose',2); optimize(Constraints, Objective, ops);

代码里我用sdpvar定义连续变量、binvar定义启停变量。注意SOC变量多定义了一列soc(:,Nt+1),存的是调度结束时SOC,用来追踪终值约束。功率平衡的等式约束里,左边是出力,右边是负荷,千万别把储能充电功率放到等式左边,否则方向反了会直接导致模型无解。

这里还要补充一个容易漏掉的终值约束:如果想让储能电站24小时后SOC回到初始值,需要额外加soc(:,Nt+1) == data.storage.soc0。但加了之后模型可能变得不可行,因为某个储能单元容量太小,夜间放完电后白天根本充不回去。我的做法是把这个硬约束改成软约束,在目标函数里加一个终值偏差惩罚,这样既保证每天SOC不会漂移太远,又不会因为个别单元容量特性导致整个调度无解。

日前模块跑完后,我会把机组启停状态、储能SOC曲线存到result.dayahead里,作为下一层的输入。这里有个小技巧:在进入日内滚动之前,先把日前SOC曲线画出来看一眼。如果SOC曲线出现很频繁的满充满放突变,第一件事不是去调算法,而是检查储能容量参数是不是忘了乘健康系数。很多次“模型跑出来结果很怪”的问题,最后都出在参数初始化上。

3.4 日内滚动优化与实时修正模块

日内滚动优化实际上是一个在每个15分钟开始时刻执行的循环。我按照仿真时间顺序来写:

state_storage.soc = data.storage.soc0; state_gen.P = result.dayahead.Pg(:,1); % 初始机组出力 for k = 1:96 % 生成未来4小时预测(这里用实测值加噪声模拟超短期预测) pred_horizon = 16; % 15min点数,4小时=16点 wind_pred = data.wind_true(k:k+pred_horizon-1) + 0.05 * randn(...); load_pred = data.load_true(k:k+pred_horizon-1) + 0.02 * randn(...); % 以日前机组的启停状态为固定整数变量,仅优化连续出力 Pg_adj = sdpvar(Ng, pred_horizon); Pch_adj = sdpvar(Ns, pred_horizon); Pdis_adj = sdpvar(Ns, pred_horizon); soc_adj = sdpvar(Ns, pred_horizon+1); soc_adj(:,1) = state_storage.soc; % 当前SOC作为初值 % 构建目标函数和约束,并求解 % 滚动窗口只取第一个时刻的决策量作为实际执行值 % 更新state_storage.soc state_storage.soc = value(soc_adj(:,2)); end

这里最需要留意的是滚动窗口的初值传递。很多第一次写滚动优化的同学会忘记把soc_adj(:,1)设为当前实际SOC,导致每个窗口都从默认初值开始算,结果事后再看SOC曲线完全对不上。另外一个容易踩的坑是:预测序列是在仿真时间窗口内连续取数,不是每个15分钟都从头开始预测。如果写法不对,相当于用全知预测做滚动优化,结果会好得不真实,论文审稿人一眼就能看出来。

实时控制层我做了个简化:在日内计划的基础上,计算系统实际频率偏差或功率偏差,然后用一个比例系数让储能做秒级补偿。这一层不涉及复杂的优化求解,只是模拟储能快速响应特性。如果你开发的是微网或配电网项目,可以把实时层升级成无模型预测控制或基于下垂控制的仿真,但作为策略验证,比例调节已经完全够用。还需要注意的是,实时控制层的输出要限幅,不能超过当前储能单元剩余可充放电空间,否则仿真里会出现负SOC这种物理上不可能的数值。

3.5 结果可视化与指标计算

代码跑完不是终点,还要把结果转化成看得懂的曲线和指标。我的Matlab脚本里最后专门有一个plot_results.m,至少输出三类图:

第一类是功率平衡图,把负荷、火电出力、风电光伏出力、储能充放电功率叠在一张图上,看出力结构是否合理;第二类是储能SOC图,把所有储能单元的SOC曲线画在一起,颜色区分单元,可以直观看到特性分布对SOC的影响;第三类是机组出力与电价关系图,看储能是否在电价低时充电、电价高时放电,是否符合“低储高发”逻辑。

画图时我一般会把字体调大、图例摆在外面,坐标轴单位写完整。Matlab默认配色在黑白打印时区分度不高,我习惯换成parula或者自定义ColorOrder。如果同一张图上要画12条SOC曲线,最好用细线加透明度,不然叠在一起根本看不清。我的做法是只挑几个有代表性的单元画线,其余单元用阴影带表示范围,这样既展示特性分布,又不会让图变成一团乱麻。

指标方面,我会统计弃风弃光率、系统运行成本、储能单元SOC标准差、峰谷差削减比例等。其中“储能单元SOC标准差”这个指标很能说明“特性分布”是否被考虑:如果调度策略忽略了特性分布,不同单元SOC会越拉越开;如果策略有效,各单元SOC应该始终保持在较窄的带内,同时不牺牲经济性。我复现时发现,加入SOC均衡惩罚后,SOC标准差能下降40%以上,系统成本只增加2%左右,这个交换非常值得。

4. 常见问题与实操排查

4.1 模型不可行怎么定位

储能调度模型最让人头疼的就是一运行就返回Infeasible problem。这不是代码语法错,而是约束之间互相矛盾。我的排查顺序是:先看功率平衡约束,把等式右边减去左边,在测试数据里手动验算某个时刻是否平衡;再看储能SOC终值约束,比如要求24小时结束时SOC等于初始值,但容量设置太小,根本充不回去,不可行就出现了;最后看机组爬坡约束,尤其在新能源骤升骤降时,常规机组爬坡速率不够,需要加弃风变量或储能调节变量。

建议在每个约束拼接处加注释,并写一个小的可行域检查脚本。更快的做法是给模型添加松弛变量:在功率平衡约束里加上正负偏差项,并给偏差项加一个极大的惩罚系数。如果求解器返回的松弛变量非零,就能快速定位是哪个时段的平衡出了问题。我上次调试一个考虑配电网潮流约束的版本时,发现松弛变量总是在晚间负荷高峰时段非零,排查后才知道是储能放电功率上限设置得太保守,导致晚间高峰时段系统功率缺额。这个排查过程用了不到十分钟,比对着约束式子猜要高效太多。

4.2 SOC

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询