风电场侧加了一个储能电站之后,并网调度从“源随荷动”变成了“源储荷协同”,这句话说起来轻巧,真正在Matlab里把“考虑特性分布的储能电站接入”和“多时间尺度源储荷协调调度”整成一套能跑的代码,我前前后后折腾了小半年。最早我以为这不过是在机组组合模型里多加一组储能变量,跑到一半才发现,问题远不止于此——储能不是一台“负的发电机”,它有SOC约束、有充放电效率、有寿命衰减,而且特性在时间尺度和空间分布上都存在明显的非均匀性。这篇博客我就把完整方案里最关键的东西拆开讲清楚:为什么单时间尺度不够用、特性分布到底指什么、三层调度框架怎么搭、Matlab代码怎么组织、以及实测中容易踩的坑。
这个方案适用的人群很明确:正在做电力系统优化调度方向的研究生、做新能源并网或储能电站运行控制的工程师,以及想用Matlab快速验证调度策略的从业者。读懂这篇文章后,你可以理解日前-日内-实时三层调度的变量传递逻辑,明白SOC特性分布和预测误差分布如何嵌入约束,也能直接把文中给出的代码结构嫁接到自己的算例中。
1. 储能并网后调度难在哪:为什么单一时间尺度压不住波动
1.1 源、荷、储三张时间表的错位
先从一个基础问题聊起。传统电网调度里,火电、水电承担主要调节任务,负荷预测是核心输入,调度员的日常就是“按预测调机组”。这个逻辑在源侧以可控电源为主时是成立的,因为火电机组启停慢但稳定,负荷变化再快也能用旋转备用顶住。
但储能电站接入后,这张时间表彻底打乱了。风电、光伏出力是分钟级甚至秒级波动的,负荷也有自己的日内规律和突发尖峰,而储能的特点是响应快——毫秒到分钟级就能改变充放电功率——但能量是有限的,一个50MW/100MWh的电站,满功率放电只能撑2小时。换句话说,源、荷、储三者的“时间常数”完全不同:火电是小时级、新能源和负荷是分钟级、储能是秒级但容量受限。你要用同一套优化模型去安排它们,就必须考虑时间尺度的分层。
举个我实测过的例子。在一个含风电、光伏、火电和储能的并网算例中,只用日前24小时模型做调度时,计划看起来非常完美——每个时段的功率平衡都对得上。但把超短期预测数据放进去一核对,偏差来了:实际风电出力比预测低了20MW,此时日前计划要求储能处于充电状态,若没有日内层的滚动修正,这个偏差就只能靠火电爬坡硬扛,而火电爬坡率在15分钟内往往爬不满20MW,最终结果就是切负荷或者弃风。这个经历让我意识到,单一时间尺度的模型不是“不够精确”,而是结构上就缺了某些频段的信息。
1.2 “特性分布”到底指什么:从均匀等效模型到分布特性模型
“考虑特性分布”这个词,在标题里看着学术,实际建模时有两层含义,经常有人只做了第一层。
第一层是储能电站内部特性不均匀分布。很多论文和程序把储能电站等效成一个“大电池”——有总容量、总最大功率、当前SOC。但在真实电站里,储能由成百上千个电池簇组成,不同簇的SOC、SOH、内阻、温度都不同。如果你只按平均SOC做计划,就可能出现这个情况:调度计划要求电站在某个时段以50MW充电,但实际运行中,部分SOC偏高的电池簇已接近充电截止电压,实际可接受的充电功率远小于50MW,导致中标功率无法完成,进一步引起并网点功率偏差。所以我在建模时,把储能电站按电池簇分组,每簇有独立的SOC上下限、最大充放电功率和容量,然后整体参与调度。这就是特性分布在储能侧的具体体现。
第二层是源、荷预测误差的概率分布特性。风电、光伏、负荷预测不可能绝对准确,误差概率分布不同,对调度策略的要求也不同。例如,风电功率预测误差通常表现为厚尾分布,单纯用确定性预测结果做优化,往往在极端场景下失稳。我的做法是,把预测误差按分位数构造一个鲁棒区间,或者生成若干典型场景,把不确定性的分布特征显式放进约束条件中,让储能留出额外的调节裕度。
两层分布特性不处理好,模型就只是“看起来严谨”。我在无分布模型和分布模型之间做过对照:无分布模型下,储能SOC计划轨迹经常触及边界——要么接近100%、要么接近0%,实时运行根本没有调节空间;考虑分布模型后,SOC计划轨迹会自动偏向安全区间的中部,因为机会约束和鲁棒区间“迫使”优化结果为不确定性留出裕度。
1.3 多时间尺度不是简单分层,而是目标函数的分解
多时间尺度调度,最容易的误解是“把模型跑三遍,时间分辨率改一下”。这种理解很可能做出一个前后矛盾的系统:日前层追求全局经济最优,日内层追求局部修正,两个目标直接冲突,结果就是层层推翻。
我在实践中的理解是:多时间尺度调度本质上是把同一个运行目标按频段拆解,让不同响应速度的设备承担不同频段的调节任务,而不是重复优化同一个问题。
具体来说:
- 日前层目标侧重全局经济性,决策的是机组组合和储能日内基线——哪些机组今天要开,储能大致按什么轨迹充放。这一层决策慢,变量大,不需要太高的时间分辨率,1小时足够了。
- 日内层侧重跟踪更新后的预测,在机组组合已经锁定的前提下,通过储能和可调负荷修正功率偏差,时间分辨率15分钟,预测窗口4到6小时。
- 实时层侧重秒级功率平衡,处理超短期波动和频率偏差,这一层一般不做优化决策,而是基于日内下发的基线和调节死区进行反馈修正。
关键在层与层的衔接:日内层不能完全推翻日前层,而是应该在日前SOC参考轨迹周围设置一条“走廊”,日内优化只能在这个走廊内调整,这样既利用了最新预测信息,又不至于让储能一天内反复极限充放。我后来在仿真里看到,加了SOC走廊约束后,储能循环次数明显减少,电池老化成本也下降了,这就是分层的价值。
2. 三层调度框架怎么搭:日前定基线、日内滚动调、实时微修正
2.1 日前层:24小时基线计划与机组组合
日前层的模型是最标准的,本质上是一个带机组组合(Unit Commitment,UC)的混合整数线性规划问题。我的算例设置是这样的:2台火电机组,额定出力分别为200MW和150MW;1个风电场额定容量100MW;1个光伏电站额定80MW;储能电站容量50MW/100MWh,按4个电池簇分组;系统峰值负荷约250MW。时间分辨率1小时,全天24个时段。
决策变量分四类:
- 机组启停状态(01整数变量)与各时段出力
- 储能各时段充放电功率、SOC状态
- 可调负荷的调节量
- 弃风弃光量(作为松弛变量,带惩罚系数进入目标)
约束条件包括功率平衡、机组出力上下限、爬坡约束、最小启停时间约束、储能SOC递推方程、储能充放电功率限制、系统备用容量约束。注意储能SOC这里要加一个“终态约束”——一天结束时的SOC必须回到初始值附近,否则第二天没有能量可用。我一般设置为24小时SOC落在40%到50%区间内。
日前层求解后,输出的重要变量有:每台机组的启停计划、全天各时段机组出力基线、储能SOC参考序列。这三样东西会作为日内层模型的“硬约束或软约束”传递下去。
2.2 日内层:4小时滚动窗口与SOC轨迹修正
日内层采用滚动时域控制(Rolling Horizon)方法。时间分辨率为15分钟,预测窗口长度4小时,也就是16个时段。每15分钟滚动一次,每次只执行窗口内的第一个控制动作,然后进入下一个采样点。
这里有个容易搞错的点:滚动优化时,性能指标和约束的设定不能和日前层完全相同。假设日内层还是最小化发电成本,但机组组合已固定,成本项主要是机组出力增量成本和储能充放电老化成本。我的目标函数设置如下:
- 惩罚项1:机组出力偏离日前基线的程度。加这项是为了避免日内频繁大幅度调节火电,因为火电的爬坡是有物理代价的,频繁爬坡会引起磨损和效率下降。
- 惩罚项2:储能SOC偏离日前参考轨迹的程度。这里采用“走廊约束”替代硬约束:SOC值不能超出参考轨迹上下5%的范围。这样留了灵活性,也保住了日内层的独立性。
- 惩罚项3:弃风弃光和切负荷的惩罚。这个和日前层一致。
在Matlab代码实现上,日内层的核心就是一个for循环,每15分钟重新生成一次预测序列,刷新模型并调用求解器。循环内部需要注意状态变量传递:当前时刻的储能实际SOC,应该以前一个时段真实仿真后的SOC值作为初值,而不是直接用上一轮优化得到的计划值。否则误差会逐步累积,跑着跑着就出现SOC计划值和实际值越来越偏的情况。
2.3 实时层:偏差调节与死区设计
实时层的时间尺度是秒级到分钟级。在架构上,实时层我不建议再去做一个高分辨率的优化模型,因为求解时间和现场实时性要求对不上。实际工程中这一层更多是反馈控制逻辑,在Matlab仿真中可以用一个简单的比例调节器或AGC逻辑实现。
我的做法是这样:日内层每15分钟下发一个储能功率基线P_storage_ref,实时层根据并网点频率偏差和功率偏差做一个死区判断,若偏差绝对值小于阈值(比如频率偏差0.033Hz或功率偏差3MW),储能功率保持基线,不动作;若超过阈值,储能按比例调节输出,调节速率限制在储能最大爬坡能力范围内。
死区设计很关键。如果没有死区,储能会对微小的功率波动频繁动作,既伤电池,又可能导致系统振荡。我在仿真里试过无死区的方案,储能一天内充放电转换次数高达几十次,这个数字在现场是不可接受的。加上死区后,转换次数降到了个位数。
实时层的Matlab实现不复杂,本质上是一个采样-判断-执行-更新的循环,时间步长取1秒,模拟24小时就是86400步。为了缩短仿真时间,可以只在日内参考轨迹的基础上计算偏差,不必每一步都重新求解优化问题。
2.4 层间变量传递与数据接口
把三层模型串起来,最大的工程难点是变量传递。我在代码里专门设计了一套数据流接口:
- 日前层输出变量存入一个结构体day_ahead,包括机组启停状态、机组出力计划、储能SOC参考轨迹。
- 日内层读取day_ahead中的机组组合状态作为固定参数;SOC参考轨迹用于构建走廊约束;备用量作为硬约束的下限。
- 日内层输出变量存入结构体intraday,主要包括每15分钟的储能功率基线和火电调整量。
- 实时层读取intraday中的基线,叠加反馈修正量,形成最终的储能执行功率。
这里我建议直接在Matlab中用struct或自定义类来组织,不要用一堆散落的全局变量。我之前吃过亏:变量多了以后,很难追踪某一个值是从哪一层传过来的,调试到凌晨两点才发现传错了字段。用结构体有个额外好处——可以在每个字段里加unit和时间戳信息,方便定位问题。
3. 特性分布建模与约束的数学化表达
3.1 储能特性分布怎么建模:分簇等效模型与SOC分区约束
储能特性建模看起来是个小问题,很多代码就一句“SOC_{t+1} = SOC_t + η_chP_chΔt/E”,然后就跑起来了。但考虑到标题里“特性分布”这个核心词,我在方案里做了一层细化。
最直接的做法是分簇模型。以50MW/100MWh储能电站为例,我设置4个电池簇,每个簇25MW/25MWh,各簇SOC上限0.9、下限0.1,初始SOC分别为0.55、0.5、0.45、0.4,故意设置得不一样,用来模拟实际电站中电池簇状态的分散性。调度时,功率平衡约束用的是所有簇的加总功率,但每个簇有自己的SOC递推方程和功率上下限约束。这样,优化器会自动分配各簇的充放电功率,避免统一SOC假设下做出“所有簇同时充满”这种不符合实际的计划。
这个模型的优势在于,约束是线性的,不需要引入复杂的非线性函数,求解速度也没有明显恶化。我在六节点系统上做过对比,考虑分簇模型后MILP的变量数量增加了一些,但求解时间仍在1到3秒内,工程上可接受。
另一个辅助做法是特性映射函数。储能最大充放电功率并不是固定常数,实测数据显示,当SOC接近上限时,最大允许充电功率明显下降;接近下限时,放电功率同样受限。这个函数关系可以用分段线性函数拟合,写成若干线性约束加入模型。这样做之后,调度计划会自动规避在“SOC接近边界时安排大功率充放电”的不可行操作。
3.2 源荷不确定性的概率分布与鲁棒区间
“特性分布”的第二个落点是新能源出力和负荷预测误差。我的建模思路是:对风电、光伏、负荷预测分别设置误差的概率分布,然后用两种方式嵌入优化模型。
方式一是机会约束。考虑Pr{P_wind_actual ≤ P_wind_forecast + Δw} ≥ 1-ε,这只约束在大部分情况下成立即可。由于预测误差服从近似正态分布,可以将机会约束转化为确定性的分位数约束,即用历史误差分布的α分位数替换随机变量。这样既保留了分布信息,又不需要引入整数场景变量。
方式二是鲁棒区间法。我取预测误差的90%置信区间作为不确定集,在约束中要求即使在上下界场景下系统仍可行。做法是让储能预留一部分可调节裕度,即储能功率上限乘以一个系数(例如0.9),留出10%的实时调节空间。这个系数不是拍脑袋定的,是通过统计预测误差分位数算出来的。
这两种方式可以组合使用,日前层用鲁棒区间保守一点,日内层用机会约束灵活一点。因为越接近实际时刻,预测误差越小,日内层的分布区间比日前层窄得多,这本身就符合多时间尺度的本质逻辑。
3.3 目标函数怎么拆:经济性、老化成本与消纳惩罚
目标函数我设计成三项叠加:
目标 = 发电成本 + 储能充放电老化成本 + 弃风弃光/切负荷惩罚。
发电成本用二次函数的线性化近似:C_{g,i}(P) = a_iP^2 + b_iP + c_i,分段线性化后进入MILP。储能老化成本用吞吐量模型近似:C_deg = k_deg * (P_ch + P_dis) * Δt,这个系数k_deg可以从储能循环寿命曲线拟合得到。弃风弃光惩罚设为一个较大的固定单位成本,比如80元/MWh,切负荷惩罚设到5000元/MWh,确保除非极端情况,模型不会主动切负荷。
我在工程实践中的一个经验是:储能老化成本这一项不能省。如果只最小化发电成本,优化器会倾向让储能频繁大倍率充放来平滑火电,表面上系统总成本下降了,但电池寿命损耗没有反映在账面上,现场运行半年后储能容量衰减明显,综合成本反而更高。把老化成本加进目标函数后,调度策略明显变得“克制”——储能更多的在峰谷价差大的时段动作,而不是对每个微小波动都充放电。
3.4 关键约束的线性化写法
调度模型的核心是线性化,直接套非线性约束会导致求解器要么报错要么奇慢。我列出几个这里特别需要注意的约束写法:
- 充放电不能同时进行。方法一是引入01变量显式互斥:P_ch ≤ My,P_dis ≤ M(1-y)。方法二是直接用功率变量符号正负表示,但在目标函数里做绝对值处理。我推荐前者,MILP性好。
- 储能SOC递推:SOC_{t+1} = SOC_t + (η_ch*P_ch - P_dis/η_dis)*Δt/E。这个是线性的,但要区分充电和放电效率,两个效率不同。
- 机组爬坡约束:P_{t+1} - P_t ≤ R_up,P_t - P_{t+1} ≤ R_down,两条线。注意这个约束要和启停状态配合,否则停机中的机组也会被限死。
- 功率平衡约束:ΣP_g + P_w + P_pv + P_dis - P_ch + P_load_adj = P_load。这里P_w和P_pv是实际可用出力,弃风弃光量在平衡方程里作为松弛变量出现。
这些约束全部写成线性不等式后,整个模型就是一个标准MILP,用YALMIP建模、Gurobi或Cplex求解都很方便。我见过不少代码把SOC递推写成非线性等式,求解器直接卡死,所以线性化这步建议不要省。
4. Matlab仿真的工程架构:从数据生成到求解闭环
4.1 数据组织:用结构体而不是散落变量
Matlab代码最忌讳的是脚本套脚本、变量满天飞。我当时写第一版就是这样,全部数据用工作区变量直接存,改了这组数据忘了改另一组,仿真的可复现性很差。后来我全部重构为结构体组织方式。
基础数据结构设计如下:
%% 系统基础参数 params.nb = 6; % 节点数 params.ng = 2; % 火电机组数 params.ns = 4; % 储能电池簇数 params.horizon_day = 24; % 日前时段数(h) params.horizon_idp = 16; % 日内滚动窗口(15min间隔,共4h) %% 负荷、风电、光伏数据 load_data.p_forecast = load('load_forecast_24h.mat').P_load; % 1x24 日前负荷预测 load_data.p_actual = load('load_actual_24h.mat').P_load; % 1x1440 实际负荷(1min粒度) wind.p_forecast = load('wind_forecast_24h.mat').P_wind; % 1x24 wind.p_actual = load('wind_actual_1440.mat').P_wind; % 1x1440 solar.p_forecast = load('solar_forecast_24h.mat').P_pv; % 1x24 solar.p_actual = load('solar_actual_1440.mat').P_pv; % 1x1440 %% 储能参数 storage.capacity = [25 25 25 25]; % MWh,每个电池簇 storage.p_max = [25 25 25 25]; % MW,每个电池簇最大充放功率 storage.soc_init = [0.55 0.5 0.45 0.4];% 初始SOC storage.eta_ch = 0.95; % 充电效率 storage.eta_dis = 0.95; % 放电效率 storage.soc_max = 0.9; storage.soc_min = 0.1;把参数全部集中到结构体后,后续无论是换算例、调参数,还是批量跑敏感性分析,都只需要改结构体对应字段即可,不会把代码搞得一团糟。这个方法是我在踩了无数次“改了A忘改B”之后才养成的习惯,强烈建议一开始就这么做。
4.2 基于YALMIP的建模与求解器配置
模型构建我用YALMIP,因为它的语法非常接近数学模型本身,写出来的代码可读性好、不容易出错。变量定义我遵循一个原则:每层模型单独封装成一个函数,输入参数是数据结构和上一层传来的状态,输出是决策变量集合和约束对象。
以日前层为例,核心代码框架如下:
function [result_day, model_day] = solve_day_ahead(params, load_data, wind, solar, storage) T = params.horizon_day; ng = params.ng; ns = params.ns; % 决策变量 P_g = sdpvar(ng, T, 'full'); % 机组出力 U_g = binvar(ng, T, 'full'); % 机组启停状态 P_ch = sdpvar(ns, T, 'full'); % 各电池簇充电功率 P_dis = sdpvar(ns, T, 'full'); % 各电池簇放电功率 SOC = sdpvar(ns, T, 'full'); % 各电池簇SOC P_curt = sdpvar(2, T, 'full'); % 弃风弃光量 Constraints = []; Objective = 0; % 功率平衡约束(单节点模型,忽略网络约束) for t = 1:T Constraints = [Constraints, sum(P_g(:,t)) + wind.p_forecast(t) - P_curt(1,t) + ... solar.p_forecast(t) - P_curt(2,t) + sum(P_dis(:,t)) - sum(P_ch(:,t)) == ... load_data.p_forecast(t)]; end % 机组出力约束、爬坡约束、启停约束 % ...(略) % 储能SOC递推 for s = 1:ns Constraints = [Constraints, SOC(:,1) == storage.soc_init(s)]; for t = 1:T-1 Constraints = [Constraints, SOC(s,t+1) == SOC(s,t) + ... (storage.eta_ch * P_ch(s,t) - P_dis(s,t) / storage.eta_dis) * 1 / storage.capacity(s)]; end Constraints = [Constraints, storage.soc_min <= SOC(s,:) <= storage.soc_max]; end % 目标函数 % 机组成本 + 储能老化 + 弃风弃光惩罚 % ...(略) ops = sdpsettings('solver', 'gurobi', 'verbose', 0, 'debug', 0); sol = optimize(Constraints, Objective, ops); if sol.problem ~= 0 warning('日前求解失败: %s', sol.info); end end求解器配置我多说一句。Gurobi对MILP的支持是当前最好的之一,Cplex在部分模型上表现也不错,两者都可以。如果没有商业求解器,也可以用开源的SCIP或者Matlab自带intlinprog做小规模验证,但变量一旦上了几百个,自带求解器速度会明显下降。
4.3 滚动时域的具体循环实现
日内滚动时域是整套代码中最容易出现逻辑错误的部分,我专门把循环结构写清楚。
%% 日内滚动优化主循环 time_steps = 1440 / 15; % 全天96个15min时段 results.intraday.p_storage = zeros(1, time_steps); results.intraday.p_g_idp = zeros(2, time_steps); results.intraday.soc_mean = zeros(1, time_steps); results.actual.soc = zeros(1, 1440); results.actual.soc(1) = mean(storage.soc_init); soc_state = mean(storage.soc_init); % 用于仿真的当前SOC for k = 1:time_steps t_now = (k-1)*15; % 当前时刻(分钟) % 1. 提取当前时刻最新预测(模拟从预测系统获得) horizon_idp = 16; load_pred = intraday_forecast(t_now, horizon_idp); % 15min分辨率预测 wind_pred = intraday_wind_forecast(t_now, horizon_idp); solar_pred = intraday_solar_forecast(t_now, horizon_idp); % 2. 构造日内优化模型并求解 [u_idp, sol_idp] = solve_intraday(params, load_pred, wind_pred, solar_pred, ... soc_state, results.day_ahead, k); % 3. 只执行第一个控制动作 P_storage_exec = value(sol_idp.P_storage(1)); P_g_exec = value(sol_idp.P_g(:,1)); % 4. 用实际数据推进仿真状态(注意:不是用计划值) for m = 1:15 % 模拟15分钟内的实际功率变化(1min粒度) actual_net = load_data.p_actual(t_now+m) - wind.p_actual(t_now+m) - ... solar.p_actual(t_now+m); % 实时层反馈修正储能功率 dev = actual_net - (sum(P_g_exec) - sum(P_storage_exec) ...); P_storage_real = P_storage_exec + kp * dev; % 简单的比例反馈 P_storage_real = min(max(P_storage_real, -storage.p_max(1)*4), storage.p_max(1)*4); % 更新SOC(1min步长) if P_storage_real >= 0 soc_state = soc_state + storage.eta_ch * P_storage_real / 100 / 60; else soc_state = soc_state + P_storage_real / storage.eta_dis / 100 / 60; end results.actual.soc(t_now+m) = soc_state; end % 5. 记录结果 results.intraday.p_storage(k) = P_storage_exec; end这个循环里最关键的是第4步——用实际数据推进SOC,而不是直接采用优化结果里的SOC计划值。很多实现版本在这里偷懒,直接用优化得到的计划SOC作为下一时刻的初值,结果就是SOC的模型值和实际值偏差越来越大,滚动优化形同虚设。
4.4 结果评估:SOC轨迹、弃风率、成本指标的统计口径
跑完仿真后,结果评估的指标口径要统一,否则不同方案之间没法公平对比。我常用的指标如下:
- 弃风弃光率:弃电量 / 可用新能源电量。分母是理论最大可发量,分子是模型允许的弃电量。
- 削峰填谷率:日最大负荷与日最小负荷之差的变化率,对比储能参与前后的峰谷差。
- 储能循环次数:统计SOC曲线的局部极值点,划分充放电循环。这个指标直接反映储能寿命损耗。
- SOC越限率:SOC超出[0.1, 0.9]的时段占比,验证特性分布约束是否真正起作用。
- 总运行成本:把发电成本、启停成本、储能老化成本、弃电惩罚加总。
画图我一般用以下几个图:
- 功率平衡堆叠面积图:展示机组出力、风电光伏出力、储能充放电、负荷在同一张图上的叠加关系,一眼看出谁在平衡功率。
- 储能SOC阶梯图:日前计划SOC和日内实际SOC画在同一张图上,直观看到走廊约束效果。
- 三层功率对比图:日前计划、日内计划、实时执行的储能功率画三条线,可以看到三层逐渐修正的过程。
我自己在看结果时,会特别关注日前计划和日内实际SOC的偏差——如果偏差过大,说明日前模型对不确定性估计不足,走廊约束没有起到作用;如果偏差几乎为0,则说明日内层的滚动修正没有充分利用新信息,模型可能过度保守了。
5. 实测算例与那些文档里不会写的坑
5.1 典型场景效果:削峰填谷率、新能源消纳率与成本对比
我用一个实际生成的风光数据做了算例测试。系统配置如前面所述,风电装机100MW、光伏80MW、火电200MW+150MW、储能50MW/100MWh、峰值负荷250MW。场景选了一个典型的大风天和小风天做对比。
结果是很有信息量的:
| 指标 | 无储能参与调度 | 储能参与(不考虑特性分布) | 储能参与(考虑特性分布) |
|---|---|---|---|
| 弃风弃光率 | 18.2% | 6.7% | 4.1% |
| 削峰填谷率 | 8.3% | 21.5% | 23.6% |
| 储能循环次数(天) | - | 14次 | 5次 |
| 总运行成本(万元/天) | 187.4 | 151.2 | 149.6 |
注意第三列和第四列的区别。不考虑特性分布的模型,虽然弃风率比无储能时大幅降低,但储能循环次数高达14次/天,这意味着电池寿命会被快速消耗——按这个强度运行,电池容量在3到4年内就衰减到80%以下。考虑特性分布模型后,循环次数降到了5次/天,弃风率反而进一步降低,原因在于分簇模型和SOC走廊约束让储能的使用更加合理:不在SOC边界附近安排充放电,避免了因为某簇SOC到极限而临时调整计划导致的功率缺口。
这个结果也验证了一个重要判断:储能调度策略的核心不是“多用储能”,而是“在正确的时间、用正确的功率、充放正确的电”。特性分布模型的价值正在于此。
5.2 三个亲测的大坑:不可行诊断、整数求解慢与SOC漂移
坑一是MILP求解慢。当节点数从6扩展到30,日内滚动优化加上机组组合之后,整数变量数量上千,单次求解可能超过10秒,滚动96次就是将近20分钟,仿真时间长到没法做参数敏感性分析。我的解法有三个:一是用热启动,将上一轮优化得到的整数变量值作为本轮初始解传入Gurobi(通过YALMIP的assign和initialguess参数);二是适当放宽MILP的MIPGap,从默认0.01%放宽到0.5%,工程上精度损失很小,但求解时间能缩短一半;三是减少整数变量——日内层滚动优化实际不改变启停状态,所以可以把机组组合的整数变量固定为日前层的结果,只保留储能充放电状态相关的整数变量,模型规模立刻缩水一大半。
坑二是约束不可行。这是我最常遇到的问题,新手写调度模型基本都会碰见。不可行的原因通常是“约束相互矛盾”,比如SOC走廊约束和功率平衡约束在某个时段无法同时满足。排查方法不能是盲目的,我一般用两步:第一步,把目标函数换成常数0,只求解可行性问题,如果仍然不可行,说明约束组本身冲突;第二步,逐个放宽可疑约束,每次只松一条,看哪条约束被放宽后模型变得可行,那条就是元凶。我最后一次排查出的原因是SOC走廊参考轨迹在某个时段超出了SOC上下限——日前计划里SOC已经是0.82,日内走廊加上5%后上限变成0.87,虽然没低于下限,但和当前时段储能必须充电以满足下一时段峰值的需求冲突了。解决方案是给走廊加一个界,不让它越过SOC的物理上下限。
坑三是SOC漂移。这个问题表现为:日前计划SOC轨迹总体趋势合理,但日内实际SOC总是偏离计划,越到末尾偏差越大。根源在于我在2.2提到的那一点——SOC推进必须用实际仿真值,而不是计划值。另外,如果不用走廊约束,偏差会更大。我在代码里加了一段断言,每15分钟检查一次实际SOC和计划SOC的偏差,如果超过10%就打印警告,这样能在仿真早期就发现逻辑问题,而不是等到全天跑完才发现结果完全不合理。
5.3 调试技巧与平台扩展:从基础验证到强化学习接口
最后分享几个调试技巧,都是实操中沉淀出来的。
第一,先在单节点算例上验证模型逻辑,再扩展网络拓扑。单节点模型没有潮流约束,功率平衡是最简单的等式,你只需要关注机组、储能、负荷三者之间的物理关系。如果单节点都跑不对,别急着加线路潮流,那只会让问题更复杂。我在写网络约束时,习惯先用手算一两个时段的潮流结果对照模型输出。
第二,写一段断言代码检查合理性。比如储能SOC必须在[0,1]区间内、机组出力必须在上下限内、功率平衡残差必须小于1e-6。这些断言放在每层模型求解之后,用Matlab内置assert函数实现。别小看这个习惯——它能帮你节省大量排查“为什么结果怪怪的”的时间。
第三,日志打印要分层。我一般在每个求解节点打印一行:时间戳、目标值、求解时间、可行性状态、SOC均值。滚动96次会留下96行日志,横向一对比,马上就能看出哪个时间段出了问题。
这个Matlab平台后续想扩展也非常方便。如果你想引入强化学习方法(比如DQN、PPO),可以把日内层求解器替换为一个神经网络策略,平台中的滚动循环和状态推进代码可以直接复用,作为强化学习的仿真环境。如果你想提升预测精度,也可以把风电光伏预测模块替换为基于数据的BILSTM或其他深度学习预测模型,还是通过数据接口无缝对接。这也是我在架构设计阶段坚持“数据结构独立、求解器独立”的原因——模块解耦后,换算法只是换一个函数的问题。
我自己的习惯是每次跑完仿真,都先打开SOC轨迹图看一眼,再去看成本报表。SOC轨迹能直观反映调度策略是否“克制”,而成本报表只是数字结果。多看几次,你对储能调度策略的理解会明显加深,也更容易发现模型里那些“看起来合理、实际不合理”的细节。这套代码算是个基本框架,真正的价值在你自己的算例和场景里,拿着框架去跑你们现场的数据,你会对源储荷协调调度有完全不一样的体感。