资讯动态

计及需求响应与电能交互的综合能源系统主从博弈调度Matlab实现

发布时间:2026/9/20 7:33:54 来源:尧图企业网站定制
最近在帮实验室师弟调一个“计及需求响应和电能交互的多主体综合能源系统主从博弈优化调度策略”的Matlab代码来来回回折腾了大半个月发现这类Leader-Follower模型表面上看就是“上层定价格、下层调负荷”两层优化实际落地时坑非常密集。如果你正在做综合能源系统调度方向的毕业设计或小论文这篇博文应该能帮你少走很多弯路。我会从模型设计、上下层目标函数与约束、求解算法选型、MatlabYalmipGurobi实现细节、算例结果解读再到常见的收敛问题和求解器报错把整个复现路径完整讲一遍。1. 整体设计思路为什么非要用主从博弈1.1 博弈参与者是谁各自在争什么先厘清一个基本问题多主体综合能源系统里的“多主体”到底指谁。最经典的配置是“一个综合能源服务商 多个用户聚合体”服务商运营燃气轮机、燃气锅炉、风电、光伏、储能等设备向用户卖电卖热同时向上级电网和天然气网买能。用户一侧也不是一个被动接受价格的负荷节点而是拥有可转移负荷、可削减负荷、甚至屋顶光伏和储能的主动响应主体。除此之外如果园区里有多个独立运营的微网或能源站彼此之间还可以直接交换电能这就是标题里“电能交互”的来源。主从博弈在这个场景里对应的是市场地位的差异服务商是领导者Leader它先公布明天的售电电价、售热电价和需求响应补贴用户是跟随者Follower看到价格后按自己用能成本最小化的逻辑去调整各时段购电、购热和负荷削减方案。服务商收到用户的响应结果后再评估自己的收益调整下一轮价格。如此反复直到双方都不愿意单方面改变策略时就得到了Stackelberg均衡解。拿生活场景类比服务商就像商场里先给商品贴价的商家用户就像逛商场的顾客顾客总会在给定价格下挑自己最满意的组合商家下一轮根据销量调价。两边都希望自己利益最大互相博弈最后稳定在一种互相妥协的定价和用能方案上。1.2 需求响应和电能交互在模型里承担什么角色很多初学者把需求响应当成一个约束条件其实它是连接上下层优化的重要变量通道。价格型需求响应体现在用户购电量会随着电价变化而偏移比如把原来集中在19点的洗衣机负荷转到23点激励型需求响应则体现为服务商发布削减补偿单价用户决策削减多少负荷来换取补偿。这两类响应都直接改变下层用户的最优解进而影响上层服务商的售能收入和设备调度。电能交互则是另一个维度的变量。传统园区模型默认所有用户只能从上级电网和服务商买电但引入电能交互后两个用户主体之间可以直接成交A用户中午光伏大发、用不完与其低价卖给电网不如以一个比电网收购价高、比服务商零售价低的内部价格卖给B用户。双方都受益这是典型的多主体P2P交易建模思路。这里要特别强调一点主从博弈和集中式优化的本质区别在于“利益归属”。集中式调度把服务商和用户合成一个整体求系统总成本最小看起来很美好但算出来的方案在市场化环境下根本没人愿意执行因为可能以牺牲某一方利益为代价。主从博弈则承认各方利益不一致通过价格信号协调决策更像真实电力市场的运行方式。2. 上下层模型的核心细节拆解2.1 上层综合能源服务商的目标函数上层模型的目标是最大化服务商在一个调度周期内的净收益常见写法max 收益 售电收入 售热收入 - 购电成本 - 购气成本 - 设备运维成本 - 需求响应补贴 电能交互净收入售电收入是各时段售电价格乘以该时段的用户购电量售热收入类似。购电成本是向上级电网购电的功率乘以分时上网购电价购气成本是燃气轮机和燃气锅炉消耗的天然气量乘以气价。需求响应补贴这一项要注意如果补贴单价给得太高用户会拼命削减负荷服务商虽然支付了补贴但省下了从电网高价买电的费用所以补贴单价本身也是博弈变量而不是随便设的一个常数。上层约束包含天然气轮机、锅炉的出力上下限爬坡速率电储能和热储能的SOC递推关系以及最重要的电功率平衡约束P_gas_turbine P_pv P_wind P_grid_buy P_storage_discharge P_p2p_in P_user_electricity P_storage_charge P_p2p_out热功率平衡约束类似燃气轮机余热回收加上燃气锅炉产热加上热储能放热等于用户热负荷。2.2 下层用户优化模型与需求响应建模下层用户的决策变量通常包括各时段购电量、购热量、可转移负荷的启动时段、可削减负荷的削减量以及参与电能交互的买卖功率。用户的目标函数是自己总用能成本最小min 用户成本 购电费用 购热费用 - 需求响应补偿 - 电能交互售电收益需求响应建模有两种常用方式。一种是弹性系数法把用户各时段负荷写成电价的函数设定自弹性系数和交叉弹性系数直接描述负荷对价格的敏感程度。另一种是分类负荷建模法更适合作为优化变量引入求解器可转移负荷保持总耗电量不变但转移区间受限可削减负荷在各时段最多削减一定比例削减后该时段负荷永久减少。我实际复现时更推荐第二种因为它能把需求响应行为写成线性约束直接交给求解器处理不用做函数拟合。下面给出一个典型的下层用户约束示意用户电负荷平衡 P_grid_buy P_pv_self P_p2p_in P_load_base P_transfer_start - P_transfer_end - P_shed P_p2p_out 可转移负荷总能量守恒 sum(P_transfer_start, t) sum(P_transfer_end, t) 可削减负荷上限 0 P_shed(t) delta_max * P_load_base(t) 购电上限、交互功率上限 0 P_grid_buy(t) P_grid_buy_max 0 P_p2p_out(t) P_p2p_max2.3 电能交互机制怎么建模才不失控电能交互建模最大的难点在价格形成机制。常见做法是设置一个内部协商电价比如取上级电网购电价和售电价的中值或者按卖家成本加成定价。更高级的做法是让内部交互电价也参与博弈迭代但在初学者复现中建议先用固定比例系数确定交互价格等基础版本跑通了再扩展。交互功率方向的处理要注意两个主体之间的交互功率如果直接用一对正负变量表示目标函数里会出现绝对值或双线性项给求解带来麻烦。稳妥的办法是为每个交互支路设置两个非负变量P_ex(t) P_ex_buy(t) - P_ex_sell(t) 0 P_ex_buy(t) P_ex_buy_max 0 P_ex_sell(t) P_ex_sell_max并在约束里加一个互斥条件防止同一时段既买又卖。用大M法或二进制变量实现互斥都可以如果允许同时买卖且有套利空间模型就会做出无意义的对倒结果没法看。2.4 容易忽略的两个约束细节第一个是储能SOC的时序递推和边界问题。24小时调度模型中储能约束要写成循环结构SOC(t1) SOC(t) eta_charge * P_charge(t) - P_discharge(t) / eta_discharge SOC(1) SOC(25) SOC_min SOC(t) SOC_max这里“SOC首末相等”这个约束很关键否则优化器会把储能能量“白嫖”掉规划出一个实际无法执行的方案。第二个是爬坡约束。燃气轮机和燃气锅炉不是水龙头出力不能瞬间跳变相邻时段出力差必须限制在爬坡速率乘时间间隔的范围内。这两个细节在初版模型中经常被漏掉漏掉后模型依然能求出解但结果会在调度图上出现突兀的跳变论文审稿人一眼就能看出来。3. 求解算法选型和Matlab代码实现3.1 两条技术路线KKT单层化 vs 迭代启发式主从博弈模型的求解在学术上有两条主流路线。第一类是Karush-Kuhn-Tucker条件法把下层优化问题用KKT条件替换并入上层问题再通过强对偶定理或大M法处理互补松弛条件和双线性项最终转化为一个混合整数线性规划问题交给Gurobi或CPLEX直接求解。这条路线数学上最严谨能稳定得到局部或全局最优解但推导过程长一旦下层存在0-1变量KKT条件就不再适用只能对连续松弛问题使用实用性受限。第二类是迭代启发式路线外层用粒子群、差分进化等智能算法搜索价格策略内层固定价格后用求解器精确求解用户优化问题再把用户响应返回外层评估服务商收益迭代更新价格直到收敛。这条路线实现简单对下层包含整数变量的问题也能处理是很多复现代码的默认选择。它的缺点是没有严格的最优性证明结果对初始价格和步长敏感需要仔细调节收敛参数。我在自己的Matlab代码里用的是“价格迭代反馈”框架服务商先给出一个初始价格向量求解下层问题得到用户负荷响应然后根据响应对价格做梯度修正再求上层问题得到新的服务商调度方案和可能的价格调整反复迭代。这样做的好处是两层各自分别调用求解器代码结构清晰出现问题容易定位。3.2 Matlab环境准备和求解器配置代码依赖Yalmip和Gurobi或Cplex。我用的是Matlab R2022a Yalmip R20230628 Gurobi 10.0。安装完成后最好先跑一遍验证命令yalmiptest能看到全部Pass后再进入模型编写。遇到“Solver not found”类报错大部分情况是求解器路径没加进来或者许可证没有正确激活。Yalmip找Gurobi依赖的是系统PATH不是当前文件夹路径这一点经常会混淆。在Windows下检查环境变量里是否包含Gurobi安装目录在Linux下则要确认gurobi目录下的lib和bin都在路径中。许可证方面只要保证License编码正确并处于有效期即可正常配置后调用ops sdpsettings(solver, gurobi, verbose, 2);verbose设为2可以在命令行看到求解过程调试阶段建议打开正式跑大批算例时再改成0。3.3 核心代码框架和关键片段先给一个主程序框架方便你理解整体流程%% 主程序双层迭代求解主从博弈 rng(0); % 固定随机种子保证实验结果可复现 T 24; % 调度时段数 max_iter 30; epsilon 1e-4; % 初始化价格 price_e0 0.55 * ones(T, 1); % 售电电价初值 price_h0 0.30 * ones(T, 1); % 售热价初值 DR_price0 0.05 * ones(T, 1); % 需求响应补贴初值 load_prev zeros(T, 1); price_e price_e0; converged false; for iter 1:max_iter % 下层给定价格求用户最优负荷 [P_user, P_heat_user, P_shed] solve_follower(... price_e, price_h0, DR_price0, params_user); % 上层根据用户响应求服务商最优调度 [profit, price_e_new, P_device] solve_leader(... P_user, P_heat_user, P_shed, params_operator); % 价格更新带步长的阻尼迭代 alpha 0.2; price_e (1 - alpha) * price_e alpha * price_e_new; % 收敛判断 if norm(price_e - price_e_old, inf) epsilon converged true; break; end price_e_old price_e; end下层用户的优化函数可以写成这样的形式function [P_user, Q_user, P_shed] solve_follower(price_e, price_h, DR_price, params) yalmip(clear); P_buy sdpvar(24, 1); % 购电 Q_buy sdpvar(24, 1); % 购热 P_shed sdpvar(24, 1); % 削减负荷 P_shift_on sdpvar(24, 1); % 转移负荷开启 P_shift_off sdpvar(24, 1);% 转移负荷关闭 % 约束 cons []; cons [cons, P_buy 0, P_buy params.P_buy_max]; cons [cons, Q_buy 0, Q_buy params.Q_buy_max]; cons [cons, 0 P_shed params.delta_max .* params.P_base]; cons [cons, sum(P_shift_on) sum(P_shift_off)]; % 可转移总量守恒 cons [cons, P_buy params.P_pv - P_shed P_shift_off - P_shift_on 0]; % 目标函数用能成本 - 需求响应补贴 objective sum(price_e .* P_buy) sum(price_h .* Q_buy) ... - sum(DR_price .* P_shed); ops sdpsettings(solver, gurobi, verbose, 0); optimize(cons, objective, ops); P_user value(P_buy); Q_user value(Q_buy); P_shed value(P_shed); end上层服务商函数类似目标函数是最大化净收益约束加入机组出力、爬坡、储能SOC和设备启停逻辑。两层函数分别封装好后主程序只需要在循环里调用即可调试时也能单独测一层。3.4 收敛判据和参数调节心得收敛判据不要只看目标函数值价格向量的变化量更敏感。我习惯同时监控两个指标价格向量的无穷范数变化量和上层收益的变化量任何一个小于阈值都视为收敛。阈值一般取1e-4到1e-3太严会拖慢迭代太松得到的均衡点不够准。价格更新公式里alpha这个步长非常关键。我一开始用alpha0.8价格飞快发散改成0.1以后收敛稳定但速度慢最后取0.2到0.3之间效果最好。如果发现价格在两个值之间来回振荡说明步长偏大或价格更新策略太激进可以加入阻尼项price_e (1 - alpha) * price_e alpha * price_e_new;这实际上是一阶低通滤波能有效抑制振荡代价是收敛速度下降。另外给价格设置上下界也是一定要做的price_e_min 0.30 * ones(T, 1); price_e_max 0.80 * ones(T, 1); price_e max(price_e_min, min(price_e_max, price_e));如果不设界迭代过程中价格可能跑到负数或极端值下层问题行域直接破掉求解器会报“Infeasible problem”。4. 算例设计和结果解读4.1 一个能跑通的典型园区参数我用的算例是“1个综合能源服务商 2个用户聚合体”调度周期24小时步长1小时。用户聚合体A是商业楼宇白天负荷高屋顶光伏覆盖约30%用户聚合体B是居民区晚高峰明显没有光伏但配备了小型储能。下面是主要参数表你复现时可以直接抄或根据你的论文数据微调参数数值电网分时购电价峰0.83元/kWh平0.49元/kWh谷0.25元/kWh服务商售电电价初值0.55元/kWh燃气轮机容量800 kW燃气锅炉容量500 kW电储能容量300 kWh最大充放电功率120 kW可转移负荷比例15%可削减负荷比例上限10%电能交互电价系数0.5内部价格 电网购电价 系数 * (零售价 - 电网购电价)收敛阈值1e-4这个配置在Gurobi下求解双层各调用一次求解器大概0.5秒以内30次迭代大约15秒跑完属于非常轻量的规模适合用来验证模型逻辑。等模型跑通后再逐步把主体数量增加到5到10个迭代时间会明显上浮但依然在可接受范围。4.2 结果图怎么画、怎么看主从博弈调度代码跑完至少要输出四类图才有说服力第一类是设备出力计划图。横轴是24个时段纵轴是功率画出燃气轮机、光伏、风电、储能充放电、购电功率的堆叠面积图直观看出系统怎么满足负荷。第二类是用户负荷响应前后对比图。把不考虑需求响应的原始负荷曲线和响应后的最终负荷曲线画在一起能明显看到峰值削减和负荷转移。算例中商业楼宇的峰值负荷响应后削减了约9%居民区的晚高峰用电有约12%平移到凌晨和中午这组数据在论文里就是需求响应有效性的直接证据。第三类是价格迭代收敛图。把每一轮迭代的售电电价画成热图或折线族随着迭代推进各时段价格应该从初值逐渐稳定。如果价格曲线一直来回摆说明还没有收敛到均衡。第四类是收益对比图。画出服务商在传统固定电价模式和主从博弈定价模式下的收益对比以及两个用户主体的成本对比说明博弈均衡使服务商收益和用户成本都处于一个相对合理的折中点。4.3 主从博弈结果和集中式优化结果有什么差别强烈建议把集中式调度作为对比算例加进去。集中式模型里服务商和用户合并为同一个目标函数最小化系统总成本约束完全一样。算完之后对比会发现两个现象集中式方案的系统总成本确实低于主从博弈方案一般低3%到8%左右这是信息完全共享带来的整体效率红利。但集中式结果里用户侧或者服务商侧很可能出现单方面利益受损的情况。我算过一组数据集中式最优解让服务商收益比主从均衡解低了15%因为系统总成本最优时电价被压得很低用户是受益方服务商没动力执行这个方案。主从博弈均衡解虽然总成本略高但两个主体都能接受这是市场机制下的必然结果。论文里写这部分对比时重点不是说哪个更好而是解释“为什么集中式不可行、主从博弈更符合市场化运行逻辑”。这个讨论能大幅提升模型的理论价值。5. 常见问题与排坑实录5.1 求解器报错“Solver not found”或“No suitable solver”这类报错九成是Yalmip没有正确识别Gurobi或Cplex。先执行solver sdpsettings(solver, gurobi); which gurobi如果输出“gurobi not found”说明Gurobi没有加入系统PATH。Windows下打开“编辑系统环境变量”把Gurobi安装目录下的bin路径添加进去然后重启Matlab。Linux下需要确认Gurobi的lib路径在LD_LIBRARY_PATH中。验证通过后在Matlab里执行yalmip(default) sdpsettings(solver, gurobi)然后随便构造一个小优化问题能正常求解就说明环境OK了。还有一个小细节Yalmip版本不要用太旧的旧版本对Gurobi 10的接口支持可能不完整。5.2 迭代震荡、不收敛怎么办价格更新流程中常见现象是电价在高值和低值之间来回跳不单调向平衡点收敛。我的排查顺序是这样第一步检查价格边界是否正常。如果上界大了最优解可能跑飞如果边界不对称求解器可能会在边界上来回弹。第二步调整步长alpha。把alpha从0.5降到0.2或者直接改成自适应步长alpha alpha0 / sqrt(iter);前期快、后期稳能改善震荡问题。第三步很关键的一步检查下层问题是不是凸问题。用户优化目标函数如果包含双线性项比如电价乘以负荷再乘以某个决策变量可行域会非凸迭代法就容易陷入局部循环。解决办法是检查目标函数里有没有两个决策变量相乘的项有的话尽量改写成线性或引入辅助变量。第四步修改初始价格。不同初始值可能导致收敛到不同的局部均衡点把初值设为电网分时购电价的1.2倍通常是个好起点。5.3 模型可行域为空的排查技巧如果两个不等式约束设置得互相冲突会出现“模型可行但上下层交替求解后全局无解”的奇怪情况。最常见的原因是可转移负荷约束写错了方向。我调代码时遇到过一次可转移负荷的开启时段被限定在8点到20点关闭时段却限定在22点到次日6点两个时段完全没有交集模型就永远找不到可行解。遇到Infeasible先用Yalmip自带的诊断工具[sol, infeasible_constraint] check(cons);它会返回不满足的约束行号。还有一个土办法把目标函数去掉只求可行解如果还无解就从约束里逐个删除某个非负限制或上下界二分定位冲突约束。5.4 储能SOC约束写了但结果不对很多复现代码的结果图里储能曲线看起来合理但细看SOC的初始时刻和末尾时刻不一致说明约束没起作用。检查约束写入时SOC约束要用循环结构加进去而不是一次性写向量for t 1:T-1 cons [cons, SOC(t1) SOC(t) eta_c * P_ch(t) - P_dis(t) / eta_d]; end cons [cons, SOC(1) SOC(T)]; % 周期边界如果写成SOC(2:end) SOC(1:end-1) ...Matlab维度可能对得上但很多时候会因为计算顺序问题导致约束根本没被正确添加。Yalmip里向量约束本身没问题但你要仔细检查等式右边使用的变量索引是否和你想的一致。5.5 结果可复现性模拟类代码最怕跑一次一个结果。Matlab里随机数生成会影响粒子群初始化或负荷采样因此在主程序最前面加一行rng(0)是保持实验可复现的最简单方法。另外建议把算例参数统一放在一个params结构体里单独写一个set_params.m文件不要散落在各个函数里。这样换一组参数跑实验时只改一个文件就行也不容易因改动一处漏了另一处。6. 实操过程中的其他踩坑记录还有几个细节想单独提一下虽然不算大坑但容易浪费半天时间。Yalmip对象不要在不同函数之间直接传递。上下层函数里都用了yalmip(clear)清空变量如果主程序在调用函数之间试图访问sdpvar对象必然报错。正确做法是函数内部构建并求解只把value()出来的数值返回主程序主程序完全不接触sdpvar变量。价格向量的维度容易搞错。如果T设为24价格变量就是24×1但如果你从Excel读数据读进来是1×24的行向量在目标函数里矩阵乘法直接报错或者默默广播出错。建议所有时序变量的维度统一用列向量读数据后转置一下。需求响应补贴不要设得过高。有一次我把需求响应补贴单价从0.05元/kWh改到0.2元/kWh结果用户直接削减了所有可削负荷服务商收益暴跌迭代过程剧烈震荡。补贴单价应该和电网峰谷价差挂钩补贴超过峰谷价差后用户的响应行为就脱离实际了。收敛判据除了看价格还要看下层用户的购电曲线在相邻两轮之间是否稳定。有时候价格已经稳定但用户内部因为整数变量切换负荷方案还在两个等价方案之间跳。这种情况可以增加一个约束用户可转移负荷在连续几轮迭代中保持同一方案或者直接忽略这种整数切换因为用户总成本和模型最终收益已经不变了。最后再分享一个我在调参时养成的习惯不要一上来就跑完整24小时和多个主体。先用3个时段、1个用户、不考虑电能交互的最小模型把迭代逻辑跑通再逐步加入时段数、第二用户和电能交互。每加一个模块就重新验证一次收敛性出了问题很容易定位。这样做虽然前期花时间多一点但整体调试效率远高于一次堆大模型然后到处找bug。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价