最近在调一套“日前日内多阶段多时间尺度源荷储协调调度”的Matlab代码,整个过程下来最大的感受是:这个方向的难点不在单层调度数学模型的复杂程度,而在两层甚至多层模型之间的衔接逻辑。很多人拿到类似的代码,要么是约束漏了某一条跑出离谱结果,要么是日前计划的输出和日内滚动优化对不上,最后对着曲线怎么看怎么别扭。这篇笔记就把我这套源代码里最关键的设计思路、数学模型、代码写法,以及调试时踩过的坑全部摊开来讲。适合正在做微电网、虚拟电厂、新能源电力系统优化方向的硕士研究生和工程师,也适合刚接触多时间尺度调度、想通过一个完整算例上手的同学。
1. 多时间尺度调度的需求拆解:为什么一天要分“两本账”
1.1 单一时间尺度的调度为什么不够用
先从一个最朴素的问题开始:能不能直接用一套24小时模型、把时间分辨率设成15分钟,一次求解搞定一整天的调度?从数学上讲当然可以,但实务中基本不会这么做。原因有两个。
第一个是预测精度问题。风电、光伏、负荷的预测误差会随着预测时长的增加而显著变大。拿风电来说,提前24小时预测的均方根误差可能达到装机容量的15%~20%,而滚动到未来1~4小时,误差通常能收敛到5%以内。如果一整天的计划都基于初始预测来做,那第20个小时的“最优解”在真实运行时刻很可能就是个偏差很大的次优解。第二个是计算规模问题。按15分钟分辨率求解24小时模型,决策变量数量是小时级模型的4倍,如果还要引入机组启停的0/1变量,混合整数规划求解时间会指数级上升,很难满足调度系统随时要出计划的实际需求。
所以工程上普遍采用分层决策的思路:先用较大时间尺度(比如1小时)做一整天的全局计划,再用较小时间尺度(比如15分钟甚至5分钟)在当天不断滚动修正。前者管“方向”,后者管“细节”。这正是“日前日内多阶段多时间尺度”这个题目想表达的核心思想。
1.2 日前、日内、实时:三个阶段的职责边界
展开来说,这套框架通常包含三个层次:
- 日前调度:提前24小时编制次日计划,时间分辨率1小时,目标是让系统在全天尺度上经济性最优,同时满足各类安全约束。它的输出是各机组出力曲线、储能充放电计划、联络线功率计划等。
- 日内调度:在当天每隔一段时间(通常1小时)滚动求解未来一个控制时域(比如4小时)的优化问题,时间分辨率15分钟或5分钟。它把最新的风电、光伏、负荷预测塞进模型,对日前计划进行局部修正,重点解决预测偏差带来的功率不平衡。
- 实时控制:在分钟级甚至秒级响应突发波动,一般通过AGC或储能快速调节完成,不属于优化调度的常规范畴。很多研究把日内滚动优化直接当成“实时”层面,实际是不太严谨的。
注意“时间尺度”这个概念有两个维度:一是模型分辨率,二是刷新频率。日前模型的输出分辨率是1小时,日内模型的分辨率是15分钟;日内模型每15分钟或每1小时重新求解一次。这两个维度要分清,写代码时数据结构才不会乱。
1.3 源、荷、储分别在优化框架里的角色
用一句大白话概括:电源负责兜底和调峰,新能源负责出力,储能负责搬移能量。
常规机组是可控的,可以上爬坡、下爬坡、启停,它的核心约束是有功出力上下限和爬坡速率。新能源的出力优先消纳,但如果不满足系统安全约束时可以弃风弃光,成本很低但对应着惩罚。储能的特点则是“时间解耦”——它把某一段时间的多余电能存储起来,在另一段时间放出,通过抬高低谷出力、降低高峰出力来创造套利空间,同时也为系统提供爬坡或备用能力。
在多时间尺度框架下,这三者的角色还会随阶段变化:日前阶段,储能承担削峰填谷和配合机组启停的任务;日内阶段,储能更多承担预测误差的快速补偿任务。这也是为什么日内模型里往往要把储能SOC偏离日前计划值的程度作为惩罚项。
2. 核心数学模型怎么搭:目标函数与约束设计
2.1 日前阶段:经济性、安全性与储能可用性的一次全局匹配
日前阶段的目标函数一般以系统总运行成本最小为目标。对于含有新能源和储能的系统,写出来大致是这个形式(以下以我代码中的线性化成本模型为例):
[ \min \sum_{t=1}^{24} \sum_{g \in G} (a_g P_{g,t} + b_g u_{g,t}) + \sum_{t=1}^{24} C_{\text{curtail}} (P_{\text{wind},t}^{\max} - P_{\text{wind},t}) + \sum_{t=1}^{24} C_{\text{es}} (P_{\text{dis},t} + P_{\text{ch},t}) ]
其中 (P_{g,t}) 是常规机组出力,(u_{g,t}) 是启停状态,(P_{\text{wind},t}) 是实际消纳的风电功率,(P_{\text{ch},t})、(P_{\text{dis},t}) 是储能充电和放电功率。注意我这里把储能成本考虑进去,是为了防止储能无意义地来回充放,实际工程中如果储能是自有的,也可以只考虑储能的折旧成本。
约束条件必须包含几条主线:
- 功率平衡约束:每个时段所有电源出力加上储能放电、减去储能充电,等于负荷需求;
- 常规机组出力上下限与爬坡约束;
- 新能源出力上限约束,出力在0与预测最大可用功率之间;
- 储能充放电功率约束、SOC递推约束、防止同时充放电的逻辑约束;
- 如果考虑网络,还要加线路潮流约束。
具体到SOC递推,这是整个模型里最容易出错的地方。写成离散递推式:
[ E_{t+1} = E_t + \eta_c P_{\text{ch},t} \Delta t - \frac{P_{\text{dis},t} \Delta t}{\eta_d} ]
其中 (\eta_c)、(\eta_d) 分别是充、放电效率。如果直接把这个方程写进YALMIP或优化工具箱,只要它是线性等式就没问题,关键是时刻注意 (E_t) 的单位要跟功率、时间粒度的乘积保持一致(一般是MWh)。
2.2 日内阶段:以“跟踪计划+微调”为目标的滚动优化
日内阶段的本质是一个带反馈的滚动优化过程。它和日前阶段最大的区别是目标函数变“软”了。如果日内阶段仍然把成本最小作为唯一目标,那么每一轮滚动求解出来的结果都会和日前计划大相径庭,导致各设备实际出力曲线看起来支离破碎,甚至机组频繁爬坡。
所以我在代码里给日内模型的目标函数增加了一个对日前计划偏离的惩罚项:
[ \min \sum_{\tau=1}^{H} \left( C_{\text{ope}} + \lambda_1 |P_{g,\tau} - P_{g,\tau}^{DA}|^2 + \lambda_2 |SOC_\tau - SOC_\tau^{DA}|^2 + \lambda_3 |P_{\text{es},\tau} - P_{\text{es},\tau}^{DA}|^2 \right) ]
这里的 (H) 是日内滚动时域,比如未来16个15分钟时段也就是4小时。(\lambda_1)、(\lambda_2)、(\lambda_3) 是权重系数,它们的大小决定日内优化是更“听话”(严格跟踪日前计划)还是更“聪明”(根据最新预测重算最优)。实际调参时,一般先让 (\lambda_1) 与运行成本系数在量纲上可比,再根据弃风/弃光率和计划跟踪偏差做取舍。
另外,日内模型中新能源出力的预测值是不断更新的,所以每一轮滚动求解时,(\tau) 对应的时间戳是真实的钟表时间,而不是从起始点重新编号。这一点很多初写代码的人会忽略,导致数据张冠李戴。
2.3 储能SOC建模与多时段状态耦合,这是最容易出错的模块
我见到的很多跑不出来的代码,问题都出在SOC约束上。这里有几个容易踩的细节:
第一个是SOC的初值问题。日前模型通常假设一天的开始SOC为一个固定值,比如0.5。但日内模型每一轮滚动时,SOC的初值必须是实际运行值,也就是上一轮滚动求解后第一个控制时段的SOC结果。如果直接用固定0.5,等于每轮滚动都让储能“失忆”,结果必然是储能持续充放电,曲线完全乱掉。
第二个是SOC的终值约束。日前模型如果不加SOC终值约束,优化器很可能会在一天结束时把储能放到最低,因为这样成本最低。这在长期运行中不可持续。常规做法是约束SOC终值接近初始值,或者限定在一个范围内,比如(0.45 \le SOC_{T} \le 0.55),也可以加一个终值偏差的软惩罚。
第三个是防止同时充放电约束。如果不加这个逻辑约束,某些求解器为了“套利”会同时让储能又充又放,从数学上这种解在同一条母线上是允许的,但物理上完全不合理。解决办法是引入一组0/1变量 (u_{\text{ch},t})、(u_{\text{dis},t}),约束:
[ 0 \le P_{\text{ch},t} \le P_{\text{ch}}^{\max} \cdot u_{\text{ch},t} ] [ 0 \le P_{\text{dis},t} \le P_{\text{dis}}^{\max} \cdot u_{\text{dis},t} ] [ u_{\text{ch},t} + u_{\text{dis},t} \le 1 ]
3. Matlab实现的关键细节:代码结构、约束写法、求解器选择
3.1 数据输入与代码分层:让数据和模型解耦
这套代码我强烈建议按模块分文件,不要把全部内容堆在一个大脚本里。我的目录结构是这样的:
project/ data/ wind_dayahead.csv wind_intraday_forecast.mat load_dayahead.csv pv_... src/ load_data.m build_dayahead_model.m build_intraday_model.m solve_and_save.m run_day_ahead.m run_intraday_rolling.m results/数据输入部分统一封装成一个load_data.m函数,输出一个结构体。比如风电日前预测、日内多轮预测、负荷曲线、机组参数、储能参数都放在同一个结构体里。这样做的好处是,后续想换算例,只需要改数据文件,不需要在模型代码里到处找地方改。我见过不少人在模型函数里硬编码数据,换一个系统就要改几十处,非常痛苦。
结构体字段的命名也要规范,比如:
data.T_da = 24; % 日前时段数 data.dt_da = 1; % 日前时间分辨率(h) data.T_id = 96; % 日内15分钟点数 data.dt_id = 0.25; % 日内时间分辨率(h) data.P_wind_max_da = ...; % 日前风电预测上限(MW),1×24 data.P_load_da = ...; % 日前负荷预测(MW),1×243.2 时间序列约束的代码写法(功率平衡、SOC递推、防同时充放电)
以YALMIP建模为例,变量定义和约束拼接其实很直观。功率平衡约束直接循环:
% 定义变量 Pg = sdpvar(repmat(n_gen,1,T_da), repmat(1,1,T_da)); % 每台机组在各时段的出力 Pch = sdpvar(1, T_da); % 储能充电功率 Pdis = sdpvar(1, T_da); % 储能放电功率 SOC = sdpvar(1, T_da); % 储能SOC(0~1) Constraints = []; for t = 1:T_da % 功率平衡:机组总出力 + 风电消纳 + 光伏 + 放电 - 充电 == 负荷 Constraints = [Constraints, ... sum(Pg(:,t)) + Pwind(t) + Ppv(t) + Pdis(t) - Pch(t) == Pload(t)]; %#ok<AGROW> endSOC递推就在循环里逐时段拼接:
% 储能初始SOC SOC0 = 0.5; Constraints = [Constraints, SOC(1) == SOC0, 0 <= SOC <= 1]; for t = 1:T_da-1 E_change = eta_ch * Pch(t) * dt_da / E_cap - (Pdis(t) / eta_dis) * dt_da / E_cap; Constraints = [Constraints, SOC(t+1) == SOC(t) + E_change]; %#ok<AGROW> end防止同时充放电用binvar定义逻辑变量:
u_ch = binvar(1, T_da); u_dis = binvar(1, T_da); Constraints = [Constraints, Pch >= 0, Pch <= Pch_max .* u_ch]; %#ok<AGROW> Constraints = [Constraints, Pdis >= 0, Pdis <= Pdis_max .* u_dis]; %#ok<AGROW> Constraints = [Constraints, u_ch + u_dis <= 1]; %#ok<AGROW>3.3 求解器调用与滚动循环的结构
求解器方面我多用CPLEX或Gurobi,YALMIP作为建模层。如果你机器上没装商用求解器,可以先装一个免费的求解器比如GLPK,但混合整数规划性能较差,大算例跑起来会很慢。建议还是用一个学术免费的Gurobi许可证,对这类小规模调度模型求解时间基本在一分钟内。
日前阶段的主程序结构不复杂:
ops = sdpsettings('solver','gurobi','verbose',2); result_dayahead = optimize(Constraints, Objective, ops); % 提取结果 Pg_DA = value(Pg); Pch_DA = value(Pch); SOC_DA = value(SOC);日内滚动优化是这套代码的灵魂。每轮滚动,需要把当前实际SOC和最新预测数据传进去,求解后只应用第一个时段的控制指令,然后推进到下一个滚动周期:
SOC_current = SOC_DA(1); % 假设从日前计划的首时段SOC开始 for k = 1:num_rolling_steps % 读取从k时刻开始的H个时段的日内预测 forecast_win = get_intraday_forecast(data, k, H); % 求解日内优化,得到未来H个时段的计划 [Pg_sched, Pch_sched, Pdis_sched, SOC_sched] = ... solve_intraday(forecast_win, SOC_current, Pg_DA, SOC_DA); % 只执行第一个时段的指令 SOC_next = SOC_sched(2); % 用模型算出的下一时刻SOC作为新的当前值 SOC_current = SOC_next; % 记录真实执行结果 store_results(k, Pg_sched(:,1), Pch_sched(1), Pdis_sched(1)); end这段代码看起来简单,里面其实藏着一个重要逻辑:“用模型算出的下一时刻SOC更新当前状态”是一种开环滚动方式;更严谨的做法是加入一个仿真/plant模型,用真实仿真值更新SOC。如果只有优化而没有仿真环境,用模型预测值更新是在可接受范围内的,但你要清楚这等价于假设模型完全准确。
4. 算例结果、调参经验与预测模块的搭配
4.1 三组算例验证:协调调度带来多少实际收益
为了验证这套代码的正确性和协调价值,我设计了三个对照场景。系统参数这样设:常规机组两台,总装机150MW;风电场装机100MW;光伏50MW;储能容量40MWh、最大充放电功率10MW;负荷峰值约160MW。
- 场景A:只有日前调度,日内不滚动。这是开环方式,完全依赖次日预测。
- 场景B:有日内滚动,但滚动时目标函数没有加上“跟踪日前计划”的惩罚项,相当于每轮都完全重新优化。
- 场景C:完整版,日前计划+带跟踪惩罚的日内滚动。
三个场景跑完的结果非常典型。从弃风弃光率看,场景A因为日前预测偏差,实际弃风弃光率约9.6%;场景B虽然实时修正能力强,但因为每轮重算,机组出力反复爬坡,调节成本反而增加,弃风弃光率约4.8%;场景C把弃风弃光率压到3.1%,同时机组爬坡动作次数比场景B少了近一半。系统运行成本方面,场景C比场景A降低约12%。
这个对比说明一个道理:日内滚动优化不是越自由越好。通过惩罚项把日内决策“锚定”到日前计划附近,既保证了实时跟踪精度,又避免了短视优化带来的设备疲劳和全局效率损失。
4.2 调试中踩过的三个坑:时间索引、SOC初值、量纲问题
第一个坑是时间索引错位。日前模型用的是24点,日内模型用的是96点,如果直接把日前计划的第t小时当成日内第t个时段,也就是默认日内96点里第1、第5、第9个点正好对应整点,这种对应关系必须通过时间戳来对齐,而不是通过下标。我用一个“时间映射表”来维护:
t_da_idx = ceil(t_id_idx / 4); % 把15分钟时段索引映射到1小时时段索引只要这里写错,日内模型在每一个滚动窗口里找计划基准值时都会找错位置,而结果曲线又不会明显异常,误差会被掩盖,排查起来非常头疼。
第二个坑是SOC初值的传递。前面提过,日内滚动时SOC初值必须是上一轮的实际SOC,而不是再设定为0.5。我自己第一次跑时,每轮滚动都把SOC初值重置为0.5,结果日内优化根本不考虑能量延续性,储能的充放电曲线在每轮边界处剧烈跳变,拉长时间看完全没有规律。这个问题一旦数据驱动或者连续滚动起来,就会放大。
第三个坑是目标函数量纲不一致。成本以“元”为单位,功率偏差以“MW”为单位,SOC偏差是个无量纲的0~1量。如果惩罚系数随便设个常数,很可能出现偏差项与成本项相差几十个数量级的情况。解决方法是先做归一化:把功率偏差除以系统总装机容量,SOC偏差除以SOC范围,再乘上一个可调的基准权重。这样你调的只是数值在0.1到10之间的权重系数,可解释性非常强。
4.3 和预测模型搭着用:BiLSTM、RVM这类时序预测该怎么衔接
调度做得再精细,如果预测数据质量差,效果终究有限。我用过几种预测模型配合这套调度代码,简单谈下它们的定位。
BiLSTM(双向长短期记忆网络)适合做日前级的风电、光伏或负荷预测。它的双向结构能同时捕捉序列前向与后向的上下文,在长时间序列中比普通LSTM更能保留早晚趋势、天气转换等特征。如果你想在Matlab里复现,可以考虑用深度学习工具箱直接搭建,训练完成后把预测结果保存成结构体,再喂给load_data.m生成调度数据。注意预测模块和调度模块之间的接口格式尽量固定,比如统一用一个data.forecast.P_WIND(t)字段,这样后面再接RVM或其他模型也很方便。
RVM(相关向量机)是多输出回归的另一个选择。它和SVM类似,但基于贝叶斯框架,能在小样本场景下给出置信区间,训练时也不像神经网络那样调一堆超参数。我习惯把RVM用在一小时级的日内滚动预测中,因为日内滚动样本量少、分辨率高,RVM比深网络更快更稳;同时它输出的方差信息可以用来动态调整日内模型里的惩罚权重——预测不确定性大的时段就把跟踪日前计划的(\lambda)调小一些,让日内优化更自由地修正,不确定性小时则反之。这个逻辑搭起来之后,整套代码的“自适应”味道一下就出来了。
如果你打算把预测代码和调度代码整合成一个完整项目,建议优先做三件事:统一数据接口、统一时间索引、统一输出单位。工程上真正难的不是把预测做得多精确,而是让预测结果在最优调度模型里能被正确使用。这套代码跑通之后,再往里面加概率约束、随机优化或者鲁棒优化,都是可以渐进扩展的。
最后再分享一个小技巧:调试时不要把目标函数一次写全,先只跑功率平衡和机组约束,看能不能得到可行解;然后逐步加入储能约束、爬坡约束、SOC惩罚项。每加一组约束,就对比一次结果曲线的物理合理性。这样定位问题会比跑通后再慢慢排查快得多。