计及需求响应的区域综合能源系统双层优化调度Matlab实现
发布时间:2026/9/30 9:22:50来源:尧图网络
这篇论文我前后复现了小半个月中间踩了不少坑。核心期刊上这类“计及需求响应的区域综合能源系统双层优化调度”的论文看起来模型都差不多可真到自己用Matlab把代码搭起来的时候卡点全在上下层耦合、KKT条件线性化和求解器配置这些环节上。这篇博文我从头到尾梳理一遍我当时是怎么拆解题目、怎么建模型、怎么求解、怎么调试的代码框架和参数都会贴出来给正在复现类似论文的读者一个可以直接参考的路径。先说清楚这个项目解决什么问题。区域综合能源系统RIES里面有很多设备——热电联产机组CHP、燃气锅炉、电锅炉、储能、光伏等等它们既供电又供热互相耦合。传统调度方式把用户负荷当成一个固定值但引入需求响应后用户会根据电价或者激励信号主动调整用能这时候用户就不再是被动接受者而是跟运营商有利益博弈的参与者。“双层优化”就是把这种博弈关系写进数学模型上层是运营商做设备出力和购能决策下层是用户根据上层给出的价格信号做负荷调整两边互相影响、迭代求解最终达到既满足用户利益、又让系统总成本尽可能低的均衡点。Matlab里做这个主流方案是YALMIP建模加CPLEX/Gurobi求解这也是核心期刊复现中最常用的技术路线。这篇内容适合三类人正在做综合能源系统优化方向的研究生准备复现或参考核心期刊仿真代码的工程师以及想把双层优化、需求响应这两个概念真正落成可运行代码的学习者。下面按我当时从零开始的推进顺序来写。1. 先拆解题目这套双层调度到底要解决什么问题1.1 “区域综合能源系统”不是把电和气放在一起就完了第一步我得弄清楚区域综合能源系统里到底有哪些元件在参与调度。最典型的配置是外部电网购电、天然气网购气、CHP机组发电供热、燃气锅炉补热、电储能削峰填谷用户侧有电负荷和热负荷可能还有分布式光伏。这些设备不是独立的核心耦合点在于CHP它烧天然气同时发出电和热电热出力之间有固定的可行域关系。这个关系在建模时通常用多边形不等式描述而不是一个简单的效率公式因为CHP在抽汽凝汽式工况下的电热出力范围是有约束的。这一点很多人忽略结果模型跑出来调度结果很奇怪——CHP电出力很大但热出力很小实际机组根本做不到。调度决策变量包括各时段CHP的电出力、热出力燃气锅炉的热出力电储能的充放电功率向上级电网购电或者售电的功率以及必要时给的备用容量。约束条件包括电功率平衡、热功率平衡、设备出力上下限、爬坡约束、储能SOC递推约束、与外网交互功率限制。这些是上层模型的主体。1.2 “双层”究竟在划清谁的权责很多读者第一次接触“双层优化”会问为什么不能把所有东西放进一个目标函数里直接求答案在于决策主体不同利益目标也不同。上层是区域综合能源系统的运营商或调度中心它决定设备出力、购能策略和售能价格目标是让系统总运行成本最小。下层是终端用户或者说负荷聚合商他们根据上层给出的价格和激励信号决定用多少电、哪些负荷可以转移、哪些负荷可以削减目标是让自己的用能费用和舒适度损失之和最小。这种结构本质是Stackelberg博弈上层是领导者先给出价格信号下层是跟随者基于这个信号做最优反应上层的决策要考虑到下层的反应不是单方面拍板。把这种关系写进优化模型就是双层规划。如果简单把用户负荷当成固定输入那就退化成单层优化需求响应等于没做。1.3 需求响应怎么落进模型里需求响应有两类常见形式建模方式也不同。价格型需求响应上层给出分时电价用户根据电价高低调整用电时段把峰时段的负荷往谷时段挪。这类响应可以用价格弹性系数矩阵来描述或者在下层模型里显式建模可转移负荷。激励型需求响应运营商会跟用户签订协议约定在特定时段用户需要削减一定量的负荷用户因此获得补偿。这类响应在下层模型里对应一个“可削减负荷”决策变量目标函数里有一项削减补偿收益同时约束削减量不能超过协议上限还要避免过度削减导致用户不满。我复现的时候用的是激励型和部分可转移负荷的混合模型因为纯价格弹性系数矩阵在实际求解时容易把负荷响应算得过于理想算出来的曲线失真。显式建模可转移负荷和可削减负荷虽然变量更多但物理意义清楚调试起来也方便。2. 模型搭建上层、下层和耦合变量的Matlab建模2.1 上层调度模型什么算成本、什么算收益上层目标函数我写成四个部分加起来再减掉售电收益购电费用从上级电网购电按外部分时电价结算购气费用CHP和燃气锅炉消耗天然气按气价结算设备运维费用各时段各设备的出力乘以单位运维成本需求响应补偿费用对用户削减负荷和转移负荷的补偿售电收益如果允许用户直接向电网售电或者在内部售电可以有收益项举个例子购电费用这一项在Matlab里用向量点乘就能算C_buy_elec sum(price_buy .* P_buy * dt);其中price_buy是外部购电价格向量P_buy是各时段购电功率决策变量dt是时段长度。注意单位统一我全程用kW和元时间用小时这样算出来的成本单位就是元不容易乱。设备运维成本类似C_om sum(k_chp * P_chp k_gb * H_gb k_es * abs(P_es));储能那块我用abs(P_es)算充放电损耗成本这里P_es为正表示放电为负表示充电。如果求解器对绝对值项支持不好可以用两个非负变量分别表示充和放。2.2 下层用户模型效用函数与满意度下层用户的优化目标是让“用电费用减去补偿收益、再加上舒适度损失”最小。舒适度损失就是削减负荷给用户带来的不便通常用削减量的二次函数表示系数越大说明用户越不愿意被削减。关键约束有这么几个各时段削减量不超过协议上限0 P_cut(t) P_cut_max(t)可转移负荷的转移量在各时段的总和保持平衡比如一天内转移出去的负荷总量等于转移进来的负荷总量削减后的实际负荷不能低于物理最低负荷避免把负荷削到零这种离谱结果下层目标函数是二次的但约束全是线性所以本质是一个凸二次规划QP。这个性质很重要因为只有当下层问题是凸问题时才能用KKT条件转成上层约束这也是后面单层化的前提。2.3 耦合关系与数据接口上下层通过什么变量耦合我需要把这条链路理清楚。上层传给下层的是各时段的电价信号和需求响应补偿价格。下层收到这些信号后求解自己的优化问题得到各时段的负荷削减量、可转移负荷的调整方案然后把调整后的负荷曲线返回给上层。上层看到新的负荷曲线后重新调整设备出力和购能计划。在代码层面这个耦合关系体现在两个地方一是在上层目标函数里需求响应补偿费用的计算依赖下层返回的削减量变量二是上层功率平衡约束里的电负荷、热负荷不再是固定参数而是“基础负荷减去削减量、再加上转移负荷”的表达式。如果不用KKT单层化而是用迭代求解数据接口就是两层之间的通信函数循环里每次更新价格和负荷。如果用KKT单层化这些耦合变量就会变成同一层模型里的变量和约束求解器一次性算出均衡点。3. 求解策略KKT单层化还是迭代嵌套3.1 三类常用求解思路对比我复现过程中查了不少相关资料双层优化的求解方法大体可以分成三类第一类是嵌套迭代法。上层用启发式算法比如粒子群、遗传算法做下层对每个个体调用求解器精确求解。优点是思路简单、代码容易理解缺点是计算量很大上层每迭代一次下层就要反复求解几十上百次而且启发式算法不保证收敛到全局最优。第二类是KKT单层化。把下层问题的最优性条件KKT条件作为约束加到上层模型里这样双层问题就变成一个单层的带互补约束的数学规划问题如果是线性或二次规划还能线性化成混合整数规划直接用CPLEX/Gurobi求解。优点是可以精确求解缺点是KKT推导和线性化过程容易出错大M参数没选好会引发数值问题。第三类是元模型或者解析反应函数法。先推导下层对上层决策变量的解析反应函数再代入上层。这个方法理论上最漂亮但对模型结构要求高实际问题很难写出解析式。核心期刊论文里最常出现的是第二类尤其在计及需求响应的背景下下层用户的QP问题完全可以KKT化。所以我复现时也走这条路。3.2 KKT条件转化与线性化细节把下层问题KKT化要写四组条件拉格朗日函数对各变量求导等于零也就是驻点条件原问题可行性条件就是下层所有约束都要保留对偶可行性条件就是所有不等式约束对应的拉格朗日乘子非负互补松弛条件比如削减量上限约束P_cut P_cut_max对应的互补条件互补松弛是非线性的这是单层化过程中最麻烦的地方。它的形式是lambda * (P_cut_max - P_cut) 0也就是说乘子和松弛量不能同时为正。非线性项不能直接交给MILP求解器要用大M法和二进制变量做线性化。具体地对不等式约束g(x) 0引入松弛变量s -g(x)则s 0。互补条件lambda * s 0等价于下面这组线性约束% 设 z 是二进制变量M是大M常数 Constraints [Constraints, s 0]; Constraints [Constraints, lambda 0]; Constraints [Constraints, s M * z]; Constraints [Constraints, lambda M * (1 - z)];当z0时lambda必须为0s可以取0到M之间当z1时s必须为0lambda可以取0到M之间。这样就把“两者乘积为0”的非线性条件拆成了混合整数线性条件。这里最要命的是M的取值。M太小会砍掉真正的可行解M太大会让约束矩阵的病态程度加剧CPLEX求解时会出现数值警告。我的做法是根据物理边界推算而不是随便取大数。比如负荷削减量上限如果是基础负荷的20%那松弛量s的上界就是该时段最大负荷的20%大M在这个量级的基础上再放大一倍就够了。3.3 关键代码片段与Matlab实现结构我最终代码的主干流程是这样的加载系统参数和负荷数据用YALMIP定义上层决策变量设备出力、购能、补偿价格定义下层KKT条件涉及的变量削减量、转移量、对偶乘子把所有约束和线性化后的互补约束拼起来定义上层目标函数其中包含需求响应补偿费用调用CPLEX求解混合整数规划后处理画负荷曲线、设备出力图、分析成本构成关键代码段大概是这个样子%% 上层决策变量 P_chp sdpvar(1, T, full); H_chp sdpvar(1, T, full); P_gb sdpvar(1, T, full); P_buy sdpvar(1, T, full); P_es sdpvar(1, T, full); % 正为放电负为充电 %% 下层用户的变量 P_cut sdpvar(1, T, full); % 各时段削减负荷 P_trans sdpvar(1, T, full); % 各时段可转移负荷正值表示转入 lambda1 sdpvar(1, T, full); % 削减量上限约束的对偶乘子 lambda2 sdpvar(1, T, full); % 削减量非负约束的对偶乘子 z1 binvar(1, T, full); % 互补约束线性化引入的二进制变量 z2 binvar(1, T, full); %% KKT驻点条件示例简化形式 % 用户目标对 P_cut(t) 求导等于零 for t 1:T Constraints [Constraints, ... -price_sell(t) alpha * P_cut(t) lambda1(t) - lambda2(t) 0]; end %% 互补松弛线性化 for t 1:T Constraints [Constraints, ... % 对应 P_cut(t) P_cut_max(t) P_cut_max(t) - P_cut(t) 0, ... P_cut_max(t) - P_cut(t) M1 * z1(t), ... lambda1(t) M1 * (1 - z1(t)), ... lambda1(t) 0]; Constraints [Constraints, ... % 对应 P_cut(t) 0 P_cut(t) 0, ... P_cut(t) M2 * z2(t), ... lambda2(t) M2 * (1 - z2(t)), ... lambda2(t) 0]; end这里alpha是用户舒适度损失函数的二次项系数。price_sell是上层给用户的售电价格在双层模型里它可以是固定参数也可以是上层决策变量。如果是决策变量上层目标函数里会多出一个二次项模型从MILP变成MIQPCPLEX也能处理但速度会慢一些建议先跑通固定价格的版本再加码。YALMIP定义变量的时候要注意full参数默认的方形矩阵定义不适合一维决策变量不写这个参数很容易在后面拼接矩阵约束时出现维度错误。求解设置方面ops sdpsettings(solver, cplex, verbose, 2, ... cplex.mip.tolerances.integrality, 1e-5, ... cplex.mip.tolerances.mipgap, 1e-3); sol optimize(Constraints, Objective, ops);跑完之后务必检查sol.problem是否为0我用一行断言或者打印来确认if sol.problem ~ 0 error(求解失败: %s, yalmiperror(sol.problem)); end这一步看似多余实际很有用。我遇到过好几次求解器返回了结果但其实是不可行条件下的近似解如果不检查problem字段后面画图和分析全在拿错误结果做文章。4. 参数设置与场景设计4.1 电价、气价和需求响应补偿参数这类项目的仿真结果很大程度取决于价格数据我建议不要随便编一组数就上。我采用的典型场景参数是分时电价峰平谷三个时段峰时段电价大约是谷时段的2到3倍这样才看得出需求响应削峰的效果。购气价格按热值折算后要让CHP在热电联产工况下有经济性优势否则调度结果会极端——全部用外购电和燃气锅炉CHP变成摆设。需求响应补偿价格的设计更讲究。补偿价格定得低用户不愿意削减负荷需求响应等于没起作用定得高运营商成本反而增加甚至比直接购电还贵。我的经验是让补偿价格处于用户舒适度损失成本的1.2到1.8倍之间。这样下层用户会积极响应但又不至于响应过度。4.2 设备参数与负荷数据的组织方式设备参数我推荐用一个结构体集中管理而不是在脚本里到处写数字。一个典型的数据结构是这样param.CHP.P_min 30; % kW param.CHP.P_max 200; % kW param.CHP.H_min 20; % kW param.CHP.H_max 180; % kW param.CHP.eta_elec 0.35; param.CHP.eta_heat 0.45; param.GB.eta 0.9; param.ES.P_max 50; % 储能最大充放电功率 param.ES.eta 0.95; param.ES.E_max 200; % 储能容量负荷数据我是用Matlab里的表对象读入可以是Excel文件或者CSV24个时段的电负荷、热负荷、可转移负荷比例和可削减负荷比例。基础负荷曲线设计时可以故意在晚高峰设一个突出尖峰这样才能在结果对比图里明显看到需求响应把它削下来的效果。4.3 对照实验与灵敏度分析的常用做法复现论文不能只跑一个场景核心期刊的论文一般要有对照实验。我这里至少跑了四个场景一是无需求响应单层优化相当于把用户负荷当固定值作为基准线。二是有激励型需求响应的双层优化就是本文模型。三是不同补偿价格下的需求响应结果观察负荷削减量和总成本的趋势。四是让储能容量或光伏容量变化做灵敏度分析。每个场景跑完之后我会记录三个指标系统总运行成本、峰时段最大负荷削减率、用户总费用变化。这些指标整理成表再画负荷曲线对比图和设备出力堆叠图。核心期刊论文里的图基本都是这个套路——上面是电负荷平抑曲线下面是各设备出力堆叠图加个成本对比表。这套后处理代码建议一开始就写好不要等模型通了再补不然中间调参时完全靠肉眼比较曲线效率太低。5. 常见问题与排错实录5.1 YALMIP/CPLEX环境问题我在环境配置上浪费了至少一天。首先声明Matlab版本直接影响YALMIP的兼容性。我一开始用的是Matlab R2021b装的YALMIP版本比较老对binvar的维度处理有问题后来换到R2023b并升级了YALMIP才解决。CPLEX我用的是12.10学术版需要注意CPLEX的官方安装包里路径不能有中文否则Matlab调用动态链接库时会报Unable to load cplexmex之类的错误。在Matlab里验证求解器是否被YALMIP正确识别就跑一行命令yalmiptest输出的列表里CPLEX和Gurobi那一行必须是found否则后面求解时YALMIP会悄悄换成内嵌求解器算得又慢又不对。5.2 模型不可行与数值病态我最常遇到的错误是求解器返回infeasible。这时候我不会直接去翻约束而是用YALMIP的assign和check命令逐条检查约束的残差assign(Constraints, sol) % 不能这么直接用正确做法是 % 手算出每组约束的松弛量看哪条违反最严重更实用的办法是先把模型拆成几块分别求解只求上层不考虑下层KKT约束或者只求下层不问上层目标确定哪一块已经不可行。我曾经发现问题是电平衡约束里的负荷表达式写错了基础负荷减去削减量之后晚高峰负荷变成负数导致功率平衡永远无法满足。这种问题单纯看求解器的报错信息根本看不出来必须自己追踪数据流。数值病态方面单位不统一是最常见的。有的变量用的是kW有的地方我一开始用MW导致矩阵条件数巨大CPLEX警告数值问题。后来我统一全部用kW和元所有约束的量级都控制在三位数以内求解顺利多了。5.3 双层迭代不收敛以及KKT单层化的坑如果读者选择用粒子群嵌套求解下层最常见的问题是双层迭代不收敛每次都震荡。我建议不要盲目加大迭代次数而是看上下层交互变量的收敛轨迹。如果负荷削减量在两个数值之间来回跳大概率是上层决策给下层的信号不连续比如价格信号在某个取值附近导致下层解剧烈变化。处理办法是给迭代过程加一个松弛因子每次只更新一部分变量lambda_user_new lambda_past rho * (lambda_candidate - lambda_past);这个思路参考了ADMM的更新思想虽然不是严格意义的ADMM但确实能改善震荡。走KKT单层化路线也有坑。第一个坑是大M值设置不当我在前面已经强调过。第二个坑是互补松弛条件的二进制变量数量会很大一个不等式约束对应一个二进制变量24时段乘上几个约束可能产生上百个二进制变量加上设备启停变量模型规模会膨胀。我优化的小技巧是能合并的约束就合并比如负荷削减量上下界可以合写成一个区间约束的两个互补条件用同一个二进制变量不同取值去表示变量数量能省不少。5.4 结果异常时的排查思路最后补充一条排查经验。有时候求解器报告的optimal value看似合理但画出来的负荷曲线或者设备出力曲线明显不合理比如储能持续满充满放或者CHP热出力突破了可行域边界。这时候要检查的是后处理代码里的取值逻辑。用value(P_chp)取决策变量数值的时候如果变量声明时用了矩阵形式取值顺序可能和预期不一致。我是通过打印每个时段的数值跟输入的负荷数据逐个对照才发现的。另一个常见问题是双层目标函数里的补偿费用和实际计算出来的削减量不匹配。原因是上层目标函数里我用的是sum(dr_price .* P_cut)但下层KKT转化后的P_cut变量在拼接约束时被重复声明了两份其中一份没有参与目标函数计算。这种重复声明错误YALMIP不会报错只会在结果里悄悄出错。排查时我用了size检查每个sdpvar变量的维度发现P_cut被声明成了2×T的矩阵问题才暴露出来。我个人实际操作中的体会是复现这类双层优化论文最考验人的不是数学推导而是把推导结果落到Matlab代码里时对变量、约束、数值量级和求解器特性的把握。建议第一次做的时候先把一个不含需求响应的简化单层模型完整跑通再逐步加入下层KKT条件和需求响应环节。每加一部分就验证一次不要一口气把完整模型堆在一起调否则出错时根本定位不了原因。这个思路我每复现一次论文都用几乎成了固定流程。后续如果想把模型扩展到多区域或者加入碳交易机制代码框架其实不用大改多加几个区域模块和对应的耦合约束就行。
网站建设高端定制企业官网