主从博弈与需求响应在综合能源系统优化调度中的Matlab实现
发布时间:2026/9/10 19:02:52来源:尧图网络
1. 从“单兵作战”到“多方博弈”这个课题到底在解决什么工程问题先说个我自己的真实感受。我刚接触综合能源系统优化调度那会儿脑子里全是“单目标优化”“全局最优”这种经典套路——把整个系统看成一个黑箱当成一个大大的MPC问题目标函数一写约束一列交给求解器就完事了。但真正做项目、跑数据、和搞工程的人聊过以后你会发现这套思路在“多主体”场景下几乎推不动。为什么推不动因为现实中的综合能源系统从来就不是单一决策者能说了算的。一个园区里可能有能源服务商负责购电、购气、运维热电联产机组有多个楼宇用户自己有光伏、储能、可调负荷他们之间既有买卖关系又有物理上的电能交互。这时候你再把整个系统当成一个整体做集中式优化就默认了所有主体的利益是统一的、信息是完全共享的这在工程实际中基本不成立。所以这个标题里的几个词每个都不是摆设“计及需求响应”说的是用户侧不是死的用户的用电行为会随着价格信号变化“电能交互”说明多主体之间不是孤立运行而是通过联络线、母线产生能量交换“主从博弈”则是从决策结构和利益关系上把运营商的定价策略和用户的用能策略放到一个非对等的博弈框架里去求解。最后一落到Matlab代码实现这事才算真正闭环了。这篇文章我就围绕这套逻辑从数学建模、博弈模型构造、KKT转换、Matlab代码实现到调参经验完整讲一遍我是怎么把这个策略从论文标题做成实际可跑的仿真程序的。适合正在做综合能源系统、虚拟电厂、电力市场方向研究的学生也适合工程上想用博弈论思路做园区多能互补调度的从业者参考。2. 需求响应和电能交互的建模从物理机理到数学约束2.1 需求响应到底怎么“进入”优化模型很多人一提到需求响应就以为是在目标函数里加了一项“需求响应成本”或者把负荷简单乘一个弹性系数这种做法太粗了。真正要落地你需要说清楚需求响应以什么形式参与是价格型还是激励型用户是怎么感知到价格并作出调整的在我这个模型里我采用的是**价格型需求响应Price-based Demand Response, PBDR**为主。核心逻辑是能源运营商制定分时电价用户看到这个电价后在满足自身用能舒适度的约束下调整各类电负荷的用能计划和购电策略。这实际上是一个典型的价格—需求双向联动机制。数学上我把用户负荷分成三类刚性负荷必须满足比如照明基础部分、可转移负荷时间上可以平移比如洗衣机、消毒设备、可削减负荷用电量和舒适度之间可以折中比如空调用电。需求响应的效果就体现在用户会根据电价高低把可转移负荷从高电价时段挪到低电价时段把可削减负荷在高电价时段适当削减。用公式表达的话用户侧的需求响应约束可以写成[ P_{load}(t) P_{base}(t) P_{shift}(t) - P_{cut}(t) ]其中(P_{base}(t))是基础负荷(P_{shift}(t))是从其他时段转入的转移负荷有正有负(P_{cut}(t))是削减量。转移负荷满足总量守恒[ \sum_{t1}^{T} P_{shift}(t) 0 ]削减量有上下限而且和用户的用电舒适度成本挂钩[ 0 \le P_{cut}(t) \le P_{cut}^{max}(t) ][ C_{DR}(t) \alpha \cdot (P_{cut}(t))^2 \beta \cdot P_{cut}(t) ]这个(C_{DR})就是用户因为削减负荷产生的不舒适成本本质上是一个凸函数。这里的(\alpha)和(\beta)是需求响应成本系数需要通过历史数据拟合我下面讲代码时会具体说怎么设初值。2.2 电能交互建模主体的物理连接与交易边界然后再看“电能交互”。这个课题里电能交互至少有两层含义。第一层是综合能源系统与外部电网的交互也就是从上级电网购电或向电网售电这部分在模型里表现为联络线功率约束[ 0 \le P_{buy}(t) \le P_{buy}^{max} ][ 0 \le P_{sell}(t) \le P_{sell}^{max} ]购售价一般取不同的数值体现“低买高卖”的市场机制否则运营商没有获利空间。第二层是多主体之间的内部电能交互也就是能源运营商和多个用户主体之间的电能交易——运营商向用户售电用户也可以在有盈余时反向馈电。这个交互的电价就是主从博弈的核心决策变量。我在建模时把电能交互处理成母线功率平衡方程的一部分。系统的电能平衡可以表示为[ P_{grid}(t) P_{CHP}(t) P_{PV}(t) P_{dis}(t) P_{load}(t) P_{ch}(t) P_{sell}(t) P_{inter}(t) ]其中(P_{inter}(t))是与其他主体交互的净功率售出为正购入为负。这就是电能交互在物理层面的耦合体现。写代码时这一条约束是所有主体互联的核心枢纽绕不开。2.3 多能源耦合电、热、气之间的“化学反应”既然叫综合能源系统不能只盯着电。我在模型里还加入了热电联产CHP机组、燃气锅炉GB和热储能TS。CHP机组的核心特征就是“以热定电”或者“电热联供”输入天然气同时输出电能和热能[ P_{CHP}(t) \eta_{CHP,e} \cdot F_{CHP}(t) ][ H_{CHP}(t) \eta_{CHP,h} \cdot F_{CHP}(t) ]天然气购买成本则取决于燃气消耗量。这里有个很重要的操作细节如果CHP以“跟随热负荷”方式运行那么它的电出力就受热需求制约在电价较高时可能无法多发如果允许“以电定热”那热负荷缺口就需要燃气锅炉来补。两种模式对应的约束表达不同运行结果也差异很大。我自己的做法是把CHP设置成可调热电比模式让优化本身去决定究竟是“以热定电”还是“以电定热”这样更灵活也更符合实际工程中的运行方式。热能侧的平衡方程是[ H_{CHP}(t) H_{GB}(t) H_{TS,dis}(t) H_{load}(t) H_{TS,ch}(t) ]热储能的状态转移方程[ S_{TS}(t1) S_{TS}(t) \eta_{ch} \cdot H_{TS,ch}(t) - \frac{H_{TS,dis}(t)}{\eta_{dis}} ]这套多能耦合约束在代码里其实不难写难的是量纲统一。热量单位是kW天然气单位是m³电价单位是元/kWh天然气价格单位是元/m³如果你不提前统一单位最后算出来成本就是一锅粥。我习惯全部折算成kW和元/kWh天然气先通过热值换算成kW再进入计算这个习惯帮我在后续调试里省了大量时间。3. 主从博弈模型拆解上层定价、下层决策与KKT转换3.1 为什么标准双层优化不能直接丢给求解器说完了物理建模接下来是博弈模型。主从博弈也叫Stackelberg博弈结构上是一个双层优化问题上层领导者综合能源系统运营商通过制定内部购售电价策略最大化自己的收益下层跟随者用户主体根据运营商给出的电价调整自己的用电计划和购电策略最小化自己的用能成本。大家第一反应可能是这不就是双层规划嘛直接套用求解器行不行答案是不行。市面上成熟的商业求解器CPLEX、Gurobi都不支持直接求解双层优化。这一点必须提前说清楚否则你会在求解器选择上卡很久。CPLEX也好Gurobi也罢他们能求解的是单层优化问题——线性规划、整数规划、二次规划但双层的嵌套结构不在他们的直接处理范围内。所以要做的工作就是想办法把双层问题转换成单层问题。3.2 下层用户的优化问题目标函数与约束表达式下层问题相对好写因为它本身是一个标准的凸优化问题。决策变量是用户的购电功率(P_{buy}^{user}(t))、各类可调负荷的调整量、储能充放电功率如果有等。目标函数是最小化购电成本加需求响应不舒适成本[ \min \sum_{t1}^{T} \left[ \rho_e(t) \cdot P_{buy}^{user}(t) - C_{DR}(t) \right] ]注意这里(\rho_e(t))是上层给定的电价下层用户只是价格的接受者这就符合完全竞争市场的假设也是Stackelberg博弈里“领导者先行、跟随者跟随”的关键。下层约束包括功率平衡约束用户的电负荷由购电、光伏、储能共同满足可转移负荷的总量守恒约束和时间边界可削减负荷的上下限储能充放电功率约束和能量状态约束。这里有个细节值得注意下层目标函数是线性的如果需求响应成本是二次的那就是二次凸规划因为(\rho_e(t))在用户看来是常数。这意味着下层问题可以放心用KKT条件转换不会遇到非凸的坑。如果用户侧的目标函数里含有非线性项比如储能老化成本是SOC的复杂函数那KKT条件会变得复杂甚至不满足凸性条件转换就会失败。所以建模阶段就要有意识地保持下层问题的凸性。3.3 KKT条件下的单层化处理与互补松弛线性化把下层问题KKT条件写出来核心包括三类第一类是下层问题的拉格朗日函数对各决策变量的偏导等于零稳定性条件。以购电功率为例用拉格朗日乘子把约束加入目标函数然后对决策变量求偏导并令其等于零。这一条把电价变量和用户购电量通过影子价格联系在了一起。第二类是原始可行性约束也就是下层自身的全部约束保持不变。第三类是互补松弛条件。这一块是编程实现时最麻烦的部分。互补松弛条件长这样[ \lambda \cdot g(x) 0 ]其中(\lambda \ge 0)(g(x) \ge 0)它是一个典型的非线性表达式。直接写进模型里会导致问题变成非线性的没法用MILP求解。标准做法是引入一个0/1二进制变量(z)把互补松弛条件线性化[ g(x) \le M \cdot z ][ \lambda \le M \cdot (1 - z) ]其中(M)是一个足够大的正数叫做“大M”。这里就有一个非常实际的坑大M如果取得太大会导致数值病态问题求解器容易陷入数值错误取得太小又会不恰当地剪掉可行域导致“最优解”其实是假的。我一般会根据实际约束的量级来定M比如功率约束的量级在百kW级别M取1000~10000就足够了不要无脑取10^9。经过KKT转换之后原来的双层问题就变成了一个单层的混合整数规划问题。如果再对上层目标函数里的双线性项电价乘以电量做强对偶转换整个问题就能变成一个标准的MILP交给CPLEX或Gurobi求解。3.4 上层运营商的收益模型双线性项和强对偶处理上层运营商的收益函数是[ \max \sum_{t1}^{T} \left[ \rho_e(t) \cdot P_{buy}^{user}(t) \rho_h(t) \cdot H_{buy}^{user}(t) - C_{gas}(t) - C_{grid}(t) - C_{om}(t) \right] ]这里的核心难点是(\rho_e(t) \cdot P_{buy}^{user}(t))这一项它是上层决策变量电价和下层决策变量购电量的乘积也叫双线性项直接处理会导致问题非线性且非凸。工程上最常用的处理方式是利用KKT条件推导出的对偶关系把双线性项转换成关于对偶变量影子价格的线性表达式。这个转换过程在理论上有点绕但实质就是把“上层收入 电价 × 电量”等价改写为“下层目标函数的对偶表达”。具体推导我建议参考经典的Stackelberg博弈论文代码层面你只需要知道这个转换做完后整个模型变成MILP可以高效求解。需要提醒一点强对偶转换成立的前提是下层问题是凸的并且满足Slater条件存在可行的内点。这意味着下层约束不能有等式冲突变量的上下界要合理。如果你发现转换后的模型无解优先检查下层原始可行性条件八成是某些约束矛盾了。4. Matlab代码实现求解链路、关键函数与运行流程4.1 代码整体框架与文件组织我在Matlab里实现这套策略时没有把所有代码堆在一个脚本里而是分了五个模块各司其职数据输入模块包括分时电价数据、负荷曲线数据电、热、光伏出力曲线、设备参数CHP、GB、储能、网络参数模型构建模块用Yalmip定义决策变量、目标函数和约束条件单层化转换模块对下层问题写KKT条件、引入大M、线性化互补松弛项求解模块调用CPLEX/Gurobi求解MILP输出结果存储为结构体结果分析模块绘制各个主体的功率曲线、温度/负荷曲线、收益对比图计算不同方案下的成本。文件组织上我一般把数据、模型和求解分开每个模块单独一个脚本或函数。这样做的最大好处是当你要换数据跑不同场景比如换一个园区、换一组负荷数据的时候不需要动模型文件只改数据文件就行。如果你把数据写死在模型里每次换案例都得从头捋非常痛苦。4.2 核心代码段逐行解析以Yalmip模型为例下面这段代码是我实际在用的模型构建框架的核心片段我贴出来并逐段解释。% 定义决策变量 rho_e sdpvar(1, T); % 运营商制定的分时电价 P_user sdpvar(1, T); % 用户的购电功率 P_chp sdpvar(1, T); % CHP电出力 H_chp sdpvar(1, T); % CHP热出力 H_gb sdpvar(1, T); % 燃气锅炉热出力 P_grid sdpvar(1, T); % 从上级电网购电功率 P_es_ch sdpvar(1, T); % 储能充电 P_es_dis sdpvar(1, T); % 储能放电 S_es sdpvar(1, T1); % 储能电量状态 bin_var binvar(1, T); % 互补松弛线性化所需的二进制变量决策变量定义好之后就到了核心的约束构建部分。这里我单独把下层用户的约束提出来因为它是KKT转换的对象。% 下层用户问题约束 Constraints_user []; % 用户功率平衡购电 光伏 储能放电 基础负荷 转移负荷 削减 储能充电 for t 1:T Constraints_user [Constraints_user, ... P_user(t) P_pv(t) P_es_dis(t) ... P_base(t) P_shift(t) - P_cut(t) P_es_ch(t)]; end % 可转移负荷总量守恒 Constraints_user [Constraints_user, sum(P_shift) 0]; % 可削减负荷上下限 Constraints_user [Constraints_user, 0 P_cut P_cut_max]; % 储能约束 Constraints_user [Constraints_user, 0 P_es_ch P_es_max]; Constraints_user [Constraints_user, 0 P_es_dis P_es_max]; for t 1:T Constraints_user [Constraints_user, ... S_es(t1) S_es(t) eta_ch * P_es_ch(t) - P_es_dis(t) / eta_dis]; end Constraints_user [Constraints_user, S_es(1) S_es_init]; Constraints_user [Constraints_user, S_es_min S_es S_es_max];这里有个细节我想强调储能约束里充放电功率不能同时大于零。在实际工程模型里最严谨的写法要引入二进制变量来保证“充电和放电互斥”。但在这类优化调度问题里如果购电价和售电价有明显差异优化结果会自动避免同时充放电因为同时充放会带来能量损耗不划算。所以为了减少二进制变量、降低求解难度可以允许优化自由选择。当然这属于一个有前提的简化如果你的模型里出现了“既充电又放电”的反常结果再补上互斥约束也不迟。4.3 双层转单层的Yalmip实现思路与调试心得最核心的单层化转换代码逻辑上分成三步。第一步把下层问题的目标函数和约束全部写出来然后对下层目标函数的决策变量求导得到稳定性条件——这一步手工推导比较繁琐但如果目标函数是线性、约束是线性的可以按标准KKT公式填空式地写出来。第二步把下拉约束里的不等式约束全部提取成标准形式(g(x) \ge 0)并为每条不等约束配一个非负对偶变量。第三步写互补松弛约束并用大M线性化。以大M线性化为例代码写法是% 以可削减负荷上限约束为例P_cut_max - P_cut(t) 0 % 对应的对偶变量为 lambda_cut_max(t) M_big 1000; % 根据量级调整 Constraints [Constraints, ... P_cut_max - P_cut(t) M_big * z_cut(t)]; Constraints [Constraints, ... lambda_cut_max(t) M_big * (1 - z_cut(t))];这段代码看起来简单但调试时最容易出问题的就是互补松弛项的方向写反。写反之后模型依然有解但解出来的结果完全不符合物理直觉比如用户会舍弃免费的可再生能源或者储能充放电行为完全紊乱。我自己就吃过这个亏后来总结了一个稳妥的做法每写完一组互补松弛约束先固定上层电价不变只求解下层问题跟直接用CPLEX求解原始下层问题的结果做对比。如果两边完全一致说明KKT转换正确如果对不上肯定是互补松弛项的方向或者大M取值出了问题。4.4 求解器选择与运行效率实测求解器方面我强烈建议用Yalmip Gurobi的组合。Yalmip负责建模Gurobi负责求解MILP两者配合非常顺畅。ops sdpsettings(solver, gurobi, verbose, 2, ... gurobi.MIPGap, 0.01, gurobi.TimeLimit, 300); result optimize(Constraints, Objective, ops);实测下来一个包含1个运营商、3个用户主体、24小时调度周期、约500个决策变量和800条约束的MILP模型Gurobi在默认参数下大概10~40秒内可以收敛到1%的MIP Gap以内。如果长时间不收敛优先检查是不是大M设置过大或者是不是有冗余的互补松弛条件导致模型过于松散。如果一定要用免费方案Yalmip也可以调内置的IntlinprogMatlab自带但求解大一点的实例会明显变慢。我个人在快速验证小规模算例时会先用Intlinprog跑通逻辑出结果无误后再切换到Gurobi做正式仿真。5. 案例仿真结果解读与参数敏感性分析5.1 典型场景设置与数据准备为了验证模型我设计了一个包含1个能源运营商和3个典型用户主体居民用户、商业用户、工业用户的仿真案例。调度周期取24小时步长为1小时。关键参数如下参数取值说明光伏装机容量200 kW用户侧CHP装机容量300 kW电出力燃气锅炉容量400 kW热出力储能容量200 kWh / 100 kW运营商侧峰时购电电价1.1元/kWh上级电网谷时购电电价0.4元/kWh上级电网天然气价格2.8元/m³按热值折算后约0.28元/kWh需求响应成本系数α0.05元/kW²·h二阶项需求响应成本系数β0.2元/kWh一次项数据这块我一般直接用Matlab生成典型日曲线基于正态分布叠加基准曲线方便快速测试。如果要做正式研究建议换成实际历史数据或标准测试系统的数据。5.2 结果呈现的核心维度这个模型跑完之后我觉得结果呈现至少要从三个维度来看。第一个维度是价格信号和负荷响应的联动。观察运营商制定的分时电价曲线和用户的负荷响应曲线。如果模型正确你会看到高电价时段用户的可转移负荷明显减少可削减负荷被削减储能开始放电低电价时段则相反。这个联动关系是需求响应是否起效的直接证据。第二个维度是收益对比。把“主从博弈策略”和“不采用需求响应的固定电价策略”做对比计算运营商总收益和用户总成本。通常结果会显示博弈策略下运营商收益提升用户成本下降或者至少持平。这种“帕累托改进”是主从博弈策略价值的核心体现。第三个维度是运行状态分析。看CHP、GB、储能的出力曲线。正常情况下CHP应该在高电价时段多发、在低电价时段少发储能应该“低充高放”GB作为热力补充只在CHP热出力不足时投入。如果这些设备行为不符合预期的经济性规律基本可以断定是模型bug而不是参数问题。5.3 敏感性分析怎么做才靠谱敏感性分析部分我最常做的三个参数是上级电网峰谷电价差、天然气价格、需求响应成本系数。例如把峰谷电价差从0.5元/kWh逐步提高到1.0元/kWh观察运营商定价和用户负荷转移的变化。正常情况下峰谷价差越大运营商利用需求响应引导用户削峰填谷的动机越强系统总运行成本也越低。但如果出现价差增大反而收益下降的反常结果那就要检查上层目标函数里有没有遗漏了购电成本项或者双线性项转换出了问题。需求响应成本系数α的变化也是一个关键的敏感性维度。α越小用户削减负荷的“代价”越低需求响应参与度就越高α越大用户越不愿意削减负荷调峰能力越差。把α从0.01调到0.1你会看到削减曲线从“尖峰削减”逐渐变成“平缓响应”。这个参数的标定直接影响模型的现实性不能拍脑袋设。6. 调试中的坑点与个人经验总结6.1 大M取值导致的“假最优解”这是我在这个项目里踩得最深的一个坑。刚开始我把大M取成了10^6结果模型解出来的结果非常离谱运营商定价时高时低用户侧储能几乎不动作但收益却异常高。检查后发现是因为大M过大导致互补松弛条件被“放宽”到了几乎不起作用的程度KKT条件没有得到真正的满足。后来我按照以下经验设置了M值取出所有涉及互补松弛的约束观察其变量的物理量级。比如功率约束里的变量是几百kW级别M取1000~5000收益函数相关的约束M取10000左右价格相关的约束M取10以内。逐个设置完再求解模型就正常了。这个教训让我意识到“足够大”的M不是越大越好而是“恰好能覆盖可行域范围”最好。6.2 互补松弛条件方向写反的“隐性错误”大M问题好歹还能从结果异常中暴露出来方向写反则是更隐蔽的坑。我最早写互补松弛约束时把变量值和对偶变量的不等式方向写反了——模型照样有解而且目标函数值看起来也合理但求出来的解根本不在原始双层问题的可行域内。后来我用“固定上层变量求解下层问题”的方法做交叉验证才发现了问题。所以我的强烈建议是写完全部KKT约束后先别急着跑整个博弈模型先验证下层转换的正确性。具体做法是固定一组电价(\rho_e(t))比如直接用上级电网的分时电价分别用“原始下层模型”和“KKT转换后的模型”求解用户的最优响应。对比两条购电曲线如果完全重合说明转换正确如果偏差明显逐条检查互补松弛条件的方向和对偶变量的符号。6.3 初值和量纲不要小看这两件小事这个模型里涉及的单位很多电功率kW、热功率kW、电量kWh、价格元/kWh、天然气费用元。如果数据文件里的单位不统一模型照样能求解但结果中各个成本项的物理含义就乱了。我的习惯是在数据输入模块的最后统一加一个“单位校验”步骤把关键变量的单位打印出来人工确认一遍再往下走。初值方面MILP求解器本身不需要初值但在迭代式的求解方案中比如用迭代法逼近主从博弈均衡初值对收敛速度和结果影响很大。我通常用“时均分配”作为初值——比如储能初始SOC设为0.5电价初值设为上级电网购电价这样模型在迭代初期的行为比较稳定不容易出现振荡。6.4 关于“均衡解唯一性”的心里话最后说一个理论层面的问题主从博弈模型的均衡解不一定唯一。理论上用户的响应函数可能是分段线性的导致上层优化有多个局部最优点。实际工程中我们一般通过求解MILP得到的是“一个”均衡解而不一定是“全局最优均衡”。这一点在论文写作中要诚实表达不要过度解读结果。在工程应用中找到一个可行且经济性良好的均衡解通常就够用了没必要纠结数学上的唯一性。我在实际项目中更倾向于多跑几组初值或者略调大M值观察解是否稳定。如果每次结果都差不多说明模型对参数比较鲁棒结果可信如果结果差异很大建议检查模型是否存在约束缺失或者存在明显的多解结构。这个课题真正做完一遍我最大的体会是理论文章里轻描淡写的一句“利用KKT条件将双层转化为单层”到代码层面其实涉及大量细节。大M怎么取、互补松弛怎么转、求解器怎么调、结果怎么验证每一个环节都能让程序崩溃或者让结果失真。但反过来说只要你把数学原理吃透、把调试的坑性掌握清楚这套主从博弈调度框架能非常实在地帮助你解决多主体综合能源系统里的利益协调问题。希望这篇文章能帮你少走一些我走过的弯路。
网站建设高端定制企业官网