做综合能源系统调度优化的同行应该都有体会:单层模型跑得再顺,真到了评审或者现场,总会被问一句"用户凭什么按你给的曲线用能?"——这正是需求响应没建模到位的典型症状。这次我完整复现了一篇核心期刊上关于"计及需求响应的区域综合能源系统双层优化调度"的工作,全程用Matlab实现,从模型推导、KKT条件转换到求解器调试都走了一遍,踩了不少坑,也有了一些自己的理解。这篇文章就是把复现过程中的模型框架、需求响应建模方法、双层求解思路和代码细节一次讲清楚。适合正在做综合能源调度研究、或者打算复现双层优化论文但苦于不知从哪下手的同学参考,内容偏工程落地,不是教科书式推导。
1. 先搞清楚双层优化到底在优化什么
1.1 单层模型为什么不够用
早期的大量调度研究都是单层模型:把电热负荷曲线当成给定的刚性需求,然后优化各设备出力,让系统总运行成本最小。这个方法思路简单、求解容易,但有一个致命问题——它默认用户对电价和激励信号完全没有反应。可现实不是这样。用户在分时电价或者可中断负荷补偿面前,是会调整用能行为的。你今天算出来的"最优"调度,放到真实场景里很可能不是最优,因为负荷端早就变了。
这就是为什么题目里强调"计及需求响应"。需求响应不是边界条件,而是一个内生变量。用户看到电价、收到激励之后,会理性地改变自己的用能计划。这种上下级之间互相影响的决策关系,单层模型表达不出来,只能上双层优化。
从数学角度看,单层是一个优化问题,双层是两个嵌套的优化问题。上层系统运营商决定设备出力和价格/激励信号,下层用户根据这些信号优化自己的用能成本,上层再根据用户的响应来评估自己的综合收益。这是一个典型的领导者-跟随者博弈。通俗讲:单层模型是"我说了算",双层模型是"你说你的,用户按自己利益最大化的方式应答,最后系统必须在博弈均衡点上运行"。
1.2 区域综合能源系统的构成
这个项目里的区域综合能源系统(RIES),覆盖电、热、气三种能量形式。核心设备包括:
- 热电联产机组(CHP),同时产电和产热,是电热耦合的关键环节
- 燃气锅炉,补足热力缺口
- 电锅炉或者热泵,实现电转热
- 储能电池和蓄热罐,承担跨时段调节
- 光伏和风电,按预测出力曲线处理
能量结构上,电母线连接电网购电、可再生出力、CHP电出力和电池充放电;热母线连接CHP热出力、锅炉出力、电转热出力和蓄热罐。负荷侧则分为刚性电热负荷和可以参与需求响应的柔性负荷。调度时间尺度是日前24小时,步长1小时,这是绝大多数调度类核心期刊采用的基本设置,复现时不建议随意改成其他粒度,否则与原文结果的可比性会明显下降。
上层目标通常包含系统运行成本、购能成本和碳排放成本,不同论文的权重形式不一样,但核心思想一致:在满足系统约束的前提下让综合成本尽量小。下层用户的目标则是用能成本最小,包括购电费、购热费,同时参与需求响应还能获得补偿收入。两层目标在这一刻天然存在冲突:系统想压低峰时负荷,用户想省自己的电费,博弈就发生在价格和激励的传递过程中。
2. 需求响应建模:价格型和激励型怎么落地
2.1 价格型需求响应的弹性系数法
价格型需求响应(PDR)最常见的落地方式是价格弹性矩阵。它描述的是:当前时段价格变化引起当前时段负荷变化的程度(自弹性),以及其他时段价格变化引起当前时段负荷变化的程度(交叉弹性)。典型表达式是:
[ \Delta L_t / L_t^0 = \varepsilon_{tt} \cdot \Delta p_t / p_t^0 + \sum_{s \neq t} \varepsilon_{ts} \cdot \Delta p_s / p_s^0 ]
在Matlab里实现这个公式,本质就是一个矩阵乘法。先构造弹性矩阵:对角线放自弹性系数(一般取负值,因为价格涨、需求降),非对角线放交叉弹性系数(一般取正值,表示负荷从高电价时段转移到低电价时段)。然后把电价变化向量乘进去,就能得到每个时段的负荷改变量。
这里有几个复现时特别容易踩的细节。第一,交叉弹性矩阵必须做对称处理,因为转移是相互的;第二,要保证转移后的负荷电量基本守恒,这需要对弹性矩阵做行和归一化,否则每天的负荷总量会凭空变化,违反能量守恒;第三,弹性系数取值有范围,电负荷自弹性一般在-0.2到-0.5之间,交叉弹性在0.01到0.1之间,取值太大会出现负荷被"转移"成负值的荒唐结果。
热负荷的需求响应和电负荷要分开建模。热负荷的响应更多依赖建筑热惯性和蓄热装置,弹性系数比电负荷小得多,不能生搬硬套同一套弹性矩阵。我在项目里就是把电负荷和热负荷的PDR逻辑分开写,热负荷的价格型响应放在蓄热罐的调度策略里体现,这样更贴近实际。
2.2 激励型需求响应的0-1变量建模
激励型需求响应(IDR)建模成可中断/可削减负荷。系统运营商会和用户签订协议,约定在特定时段可以切断部分负荷,同时支付补偿费用。数学上需要引入0-1变量表示"该时段是否调用削减",以及连续变量表示"削减多少"。
约束通常包括三类:单时段削减比例不能超过该时段总负荷的15%左右;单日累计削减量要设上限,避免用户被过度调度;连续削减持续时间也要限制,比如最多连续削减2个小时。这些约束引入以后,模型从线性规划升级为混合整数线性规划(MILP),求解难度明显上升。所以很多人不不愿意加这类约束,但核心期刊里的需求响应如果不加IDR,评审通常会质疑建模完整性。
IDR的补偿价格也要谨慎设置。我在复现时试着把补偿单价设得比峰时电价还高,结果系统反而在峰时调用大量削减,总成本不降反升。这说明激励价格不是越高越好,它存在一个临界点,在临界点上系统愿意调用削减、同时又不造成过大的补偿支出。这个临界值需要根据电价曲线和负荷特性来标定,也是复现时调参的乐趣所在。
2.3 需求响应对调度结果的实际影响
需求响应有两个层面影响调度结果。第一层,负荷曲线本身的峰谷差被改善,使得CHP和储能设备不再需要频繁地大幅度调节出力,设备运行更平稳。第二层,价格和激励作为上层决策变量,会影响下层的负荷分布,而负荷分布又决定上层设备出力,这就形成强耦合。如果需求响应参数设置合理,你会看到调度结果里峰时的燃气锅炉出力明显降低,低谷时电锅炉和蓄热罐的充能比例提高,整体运行成本下降。
我在调参过程中记录过一组对比:没有需求响应时,系统日运行成本大约是某个基准值;加入PDR后,成本下降约4%到6%;再叠加IDR后,成本继续下降约2%到3%,同时碳排放也有所下降。这个量级的变化和原论文趋势一致。如果复现时需求响应前后成本没有任何变化,优先检查是不是价格信号的波动区间太窄,用户端根本没感受到差异。
3. Matlab代码实现:从框架到细节
3.1 代码结构怎么组织
复现这类论文,最忌讳上来就写一个几百行的巨型脚本。我采用的文件结构是:
main.m:主脚本,加载数据、调用建模求解、保存结果data_input.m:所有参数和预测数据初始化model_upper.m:上层模型的目标函数与约束model_lower.m:下层用户模型以及KKT条件转换solve_bi_level.m:把双层转化为单层MILP并调用求解器plot_result.m:出图,电功率平衡图、热功率平衡图、DR前后负荷对比图
这个结构的好处是:论文里每一节模型都能对应到代码里的一个文件,调参数时不用翻来翻去,排查问题时也可以分层定位。我先搭好这个骨架,再往里面填公式,效率比从零写一个巨型脚本高得多。这是我复现了多篇调度类论文后总结出来的经验,强烈建议照这个思路组织。
3.2 双层优化三种主流求解方法
双层优化在工程上常用三种解法:
第一种,KKT条件加强对偶定理,把下层问题等价替换成约束,并入上层问题,整体变成单层MILP。这种方法有严格的数学推导作为支撑,是最推荐的复现路径,本项目用的也是它。
第二种,启发式嵌套。外层用粒子群或者遗传算法优化上层决策变量,内层调用CPLEX或者Gurobi解下层模型。这种方法理论上简单,但外层群智能算法的收敛性没有保证,每次跑的结果可能不完全一样。
第三种,交替迭代。上下层互相代入对方的最优解,反复迭代直到收敛。难点在于不保证单调收敛,实际使用时要加松弛因子和收敛判据。
对于复现核心期刊工作,我更推荐第一种,因为期刊论文通常也会用这类严格方法,结果可复现性最强。KKT转换之后的单层MILP直接用Gurobi求全局最优,省去很多关于"启发式算法是不是找到局部最优"的争议。
3.3 KKT条件转换的完整流程
把下层问题写成标准拉格朗日形式,然后按四步走:
第一步,写下层问题的拉格朗日函数,包含目标函数、等式约束(如功率平衡)和不等式约束(如设备容量限制)。
第二步,令拉格朗日函数对各变量求偏导等于零,得到平稳性条件。这一组等式直接作为上层模型的额外约束。
第三步,加入互补松弛条件,并明确对偶变量的非负约束。互补松弛条件的每一个乘积项,都要用Big-M法线性化,引入0-1辅助变量。
第四步,处理上层目标函数里出现的下层最优目标函数值。用强对偶定理,把下层目标替换成它的对偶表达式,这样就把双线性乘积项消掉。如果上下层耦合里还存在价格和负荷的乘积项,也需要通过线性化处理,常见做法是引入辅助变量加Big-M约束。
注意:Big-M法里的M值不能取得过大。M太大虽然理论上满足约束要求,但会让系数矩阵产生病态,CPLEX或Gurobi要么求解极慢,要么直接报数值错误。我在实际代码里从1e3开始试,逐步增大,找到既能收敛、又不影响精度的最小值。这个调M的过程在整个复现里占了不少时间,也是很多新同学卡住的地方。
3.4 关键代码片段
变量定义部分,最基础的写法是这样:
P_chp = sdpvar(1, 24); % CHP电出力 H_chp = sdpvar(1, 24); % CHP热出力 P_gb = sdpvar(1, 24); % 燃气锅炉热出力 P_eb = sdpvar(1, 24); % 电锅炉电功率 P_chg = sdpvar(1, 24); % 电池充电功率,非负 P_dis = sdpvar(1, 24); % 电池放电功率,非负 H_tes_c = sdpvar(1, 24); % 蓄热罐充热功率 H_tes_d = sdpvar(1, 24); % 蓄热罐放热功率 P_buy = sdpvar(1, 24); % 向主网购电 P_cur = sdpvar(1, 24); % 激励型需求响应削减量 u_cur = binvar(1, 24); % 该时段是否启用削减,0-1变量关于电池,我多说一句:工程上最好把充放电拆成两个非负变量,再用二元变量防止同时充放电,即 ( P_{dis} \le M \cdot u_{dis} ),( P_{chg} \le M \cdot (1-u_{dis}) )。不要图省事用一个有符号变量,否则可能出现充电和放电同时为正的物理荒谬结果,审稿人看到这种细节印象分很低。
电功率平衡约束是这样写的:
F = [F, P_chp + P_wt + P_pv + P_buy + P_dis + P_cur == P_load_dr + P_eb + P_chg];其中P_load_dr是经过价格型需求响应调整后的电负荷,它的表达里包含上层定的电价变量,这就是上下层耦合的根源。
求解器设置我推荐这样:
ops = sdpsettings('solver', 'gurobi', 'verbose', 2); ops.gurobi.MIPGap = 0.01; ops.gurobi.TimeLimit = 7200; optimize(F, obj, ops);MIPGap设成1%是工程上很常见的做法,不建议设成0.0001,否则可能跑几个小时没有结果。TimeLimit取7200秒是防护性设置,防止模型写错之后无限求解。调试阶段先把TimeLimit设成300秒,快速验证模型结构是否正常,等确认没问题了再放开。
3.5 结果提取与正确性验证
求解完要先检查求解器状态,不能盲目信结果。YALMIP里最简单的判断是:
if sol.problem ~= 0 error('求解失败,请检查约束和参数'); end结果合理性验证,我习惯从几个物理规律入手。第一,电池的SOC曲线应该低充高放——电价低谷充电、电价高峰放电,如果SOC曲线和电价曲线同涨同跌,说明模型方向错了。第二,CHP机组在电价高的时段应该多发,低谷时段少发,燃气锅炉只是补缺口。第三,蓄热罐应该呈现低谷蓄热、高峰放热的趋势。如果这三条基本规律都满足,模型大概率是对的。
出图方面,我一般画四张:电功率平衡堆叠图、热功率平衡堆叠图、DR前后电负荷对比曲线、储能SOC和蓄热罐状态曲线。这四张图基本能覆盖一篇调度论文核心结果,用来和原论文结果图做直观对比非常方便。
4. 参数配置与边界条件处理
4.1 设备与需求响应的关键参数
复现论文时参数是最头疼的一块,因为很多论文参数给得不全。我这里整理了一套常用典型值,是基于多篇同类文献和实际工程经验补齐的:
| 参数 | 典型取值 | 说明 |
|---|---|---|
| CHP电效率 | 0.35 | 电出力与输入天然气之比 |
| CHP热效率 | 0.45 | 热出力与输入天然气之比 |
| 燃气锅炉效率 | 0.85 | 热出力与输入天然气之比 |
| 电池容量 | 400 kWh | 按系统规模调整 |
| 电池充放电效率 | 0.95 | 双向效率,单次约0.95 |
| 蓄热罐容量 | 600 kWh | 热侧平移能力 |
| 电负荷自弹性 | -0.3 | 当前时段价格对当前时段负荷的影响 |
| 电负荷交叉弹性 | 0.05 | 价格对其他时段负荷的影响 |
| 可中断负荷比例上限 | 15% | 单时段最大可削减比例 |
| 削减补偿单价 | 0.6元/kWh | 一般高于分时电价均值 |
这个表可以当作起点,但不同论文的系统规模不同,容量和电价的绝对值会有差异,效率值和弹性系数相对更通用。关于补偿单价,我做过多组对比,峰时电价大约0.9元/kWh时,补偿单价取0.5到0.7元/kWh之间效果最好。低于这个范围,用户不愿意被削减;高于0.8元/kWh,系统支付的补偿总额会吃掉峰谷平衡带来的收益。
4.2 网络简化与电网交互边界
"区域"二字决定了网络建模的简化程度。区域综合能源系统在大多数调度类期刊工作中都被简化成单节点模型,不涉及配电网的潮流和安全约束,热网也只考虑热功率平衡,不考虑水力工况和水温动态。复现时如果发现求解时间异常长,先检查是不是自己加了太多的网络约束。单节点模型算法复杂度低得多,这也是调度型论文的常见选择。
与主电网交互的约束也要注意。系统可以向主网购电,也可以向主网售电,但购电价和售电价往往不同,而且要分别设置购电功率上限和售电功率上限。常见做法是加两个约束:( P_{buy} \le P_{buy}^{max} ) 和 ( P_{sell} \le P_{sell}^{max} ),同时用二元变量保证不同时购售电。如果论文允许用简化假设,可以用一个大数M来约束购售电不同时为正。
天然气购买方面,区域系统通常按固定管道供气处理,不考虑气网压力约束,只要天然气价乘以消耗量计入成本即可。这个简化在园区级项目中是合理的,但如果研究对象扩展到跨区域的气网,就必须增加管网模型了。复现时先对齐论文的假设边界,再决定要不要扩展。
4.3 冷负荷与热负荷的处理方式
有些区域综合能源系统还会考虑冷负荷,比如办公楼或者数据中心。本项目里只涉及电和热两种负荷,冷负荷通过电制冷机消耗电力来满足,本质上已经包含在电负荷里,不需要单独建冷母线。如果你复现的论文里有冷热电联供(CCHP),那就需要再增加一条冷母线,并加入吸收式制冷机和电制冷机的模型。从电角度讲,冷负荷的存在会增加夏季电力需求,会让电价的峰时负荷压力更大,需求响应在这种场景下价值更明显。
5. 常见问题与排查技巧实录
5.1 求解器返回"无可行解"怎么办
这是复现双层优化时最常遇到的情况,通常在下层KKT转换或者边界约束暴力添加之后发生。最快的定位方法是开启YALMIP的debug模式:
ops = sdpsettings('debug', 1, 'solver', 'gurobi');debug会直接告诉你哪几条约束互相冲突。我复现时遇到过两个典型场景。第一个,价格型需求响应后的负荷出现负值——因为弹性系数设得过大,转移过头了。解决方法是给DR后的负荷强行加非负约束,同时把弹性系数调整回合理区间。第二个,下层KKT转换后漏掉了某条互补松弛条件,导致下层最优性保证不完整,求解器找不到同时满足上层的可行解。这时候只能回头把下层的KKT条件逐条列出来打勾,一条条核对,漏一条都不行。
5.2 目标函数值与原论文差异很大
复现类项目必然面对"和原文差异"的问题。我经验是先分清楚差异来源。如果模型结构完全一致,差异多半出在参数和边界条件上。尤其是那些原文没写清楚的数据,比如分时电价的具体数值、可再生出力的具体曲线、储能初始SOC,这些都会直接影响目标函数绝对值。不要一上来怀疑自己的模型错了,先把参数对齐,跑一版"宽松"的模型看结构是否正确。
还有一种很隐蔽的情况:目标函数里成本项的单价单位不统一。天然气价到底是元/立方米还是元/kWh,换算错一位就是十倍差距。检查这类问题时,最有效的办法是手工算一个时段的成本,和代码输出对比,定位差异到底在哪个项上。
5.3 求解时间过长
双层转单层后问题规模会膨胀,MILP变量数量可能轻松突破几千个。如果求解时间无法接受,按顺序排查:
第一,确认模型里没有多余的非线性项。曾经我发现一个项目里把价格变量和负荷变量直接相乘,导致CPLEX在非线性分支上反复试探,换成Big-M线性化之后求解时间从半小时降到了两分钟。
第二,Big-M的M值是否过大。M从1e5改成1e3之后,很多病态问题会直接消失。
第三,尝试手动给出一个初始可行解。用单层模型的结果作为初值,可以让MIP分支定界从更好的起点开始搜索。
我在项目里强烈建议"分阶段构建":先用固定负荷把单层设备调度代码跑通,验证设备调度规律;然后加入需求响应,但暂不开启双层博弈,用固定电价算一遍;最后才把价格作为决策变量,走完整的双层转化流程。这样每个阶段都有明确的可解释输出,一旦出错能直接定位到具体环节,而不是在几百行规则里大海捞针。
6. 复现这类论文的方法论与扩展方向
6.1 复现论文的实操心得
复现核心期刊论文,我习惯按四步走。第一步,把论文里的模型表达式全部手抄一遍,从前到后推导。这个笨环节不能跳。双层优化的文章,如果不自己把下层KKT条件推一遍,代码里的约束写错了自己根本看不出来。
第二步,把论文所有参数整理成一张Excel表,缺失的根据同类文献的常用值补齐,同时记录补充依据。最后和原文结果做对比时,这些参数记录是溯源的重要线索。
第三步,单层先跑通。用固定负荷加固定电价,先看设备调度结果是否符合基本物理规律。CHP在峰时多发、谷时少发,电池低价充高价放,这些规律确认之后,再进入双层。
第四步,按约束的复杂度逐步回归。每加一个约束就保存一版可运行代码,保证随时可以回退。这不是浪费时间,是给自己留后路。我见过太多复现项目死在"最后一步模型不收敛"上,因为中间没有存档,根本找不到是哪条约束引入的矛盾。
6.2 从复现到扩展的思路
复现只是起点,好的框架天然方便扩展。我看到的常见扩展方向有这么几个:
第一个方向,加碳交易机制。目标函数里引入碳配额、碳价和碳排放权交易成本,分析碳约束对调度策略的影响。当前这个方向非常热门,期刊接受度也高,做法就是给设备碳排放量加一个碳成本项,再把总排放量限制在配额范围内,超出部分按碳价购买。
第二个方向,把可再生出力的预测误差建模成随机场景或者不确定区间,用两阶段随机规划或者鲁棒优化替换确定性模型。这会显著增加问题复杂度,但更贴近真实运行条件。
第三个方向,升级为三层博弈结构:电网公司、区域综合能源运营商、终端用户各占一层,讨论多级市场之间的协同问题。理论价值更高,求解也难很多,适合想深入做机制设计的同学。
第四个方向,往分布式求解靠拢。用交替方向乘子法(ADMM)把集中式双层问题分解成多个子问题,各子问题独立求解再通过迭代达成共识。这个方向与未来园区多主体自治运行的趋势很契合,工程落地潜力更大。
我自己后续打算往碳交易和鲁棒优化的交叉方向走。现在园区级项目问得最多的问题往往不是"昨天怎么调度",而是"在光伏出力预测不准、碳配额收紧的情况下,明天怎么保证运行成本和碳排放都可控"。这个框架只要在参数和约束上做扩展就能承接住这类需求,复现的价值也就从"看懂别人的论文"变成了"建立自己的可复用模型库"。
最后再分享一个我个人的小习惯:跑这种双层优化,我会把每一版模型的求解时间、目标函数值、gap收敛情况全部记录在一个日志里。复现过程中,这些看起来不起眼的记录,在最后对比分析和写技术报告时会救你一命。关于需求响应参数对结果的影响,我做过的敏感性排序是:弹性系数影响最大,其次是可中断负荷比例上限,最后才是补偿单价。调参时按这个顺序来,可以少走很多弯路。