我先交代一下背景。这个项目是我在实际课题里被问到最多的一类问题——多主体综合能源系统、需求响应、电能交互、主从博弈四个关键词堆在一起,看着像四座山,但真正落地成Matlab代码时,核心就一句话:谁先出招,谁后应对,怎么收敛到均衡点。这篇博文我尽量不把通用建模教科书复述一遍,而是把整套思路、建模细节、Matlab实现的关键节点,以及我实际跑代码时踩过的坑,都摊开来讲清楚。
1. 项目概述与整体设计思路
1.1 为什么选择主从博弈框架
先说说这类问题最本质的背景。传统电力系统调度默认假设所有参与者都是"听从调度指令"的单一整体,用集中式优化就能拿到全局最优解。但综合能源系统发展到多主体阶段后,情况就不一样了:园区里有综合能源运营商,楼下有商业用户、工业用户、居民用户,不同主体都有自己的利益诉求。运营商想通过制定能源价格让自己的收益最大化,用户呢,则会根据运营商给出的价格,调整自己的用电、用热行为——这就是需求响应。
两者之间存在明显的"先后顺序"和"利益冲突"。运营商先定价格,用户后调整用能计划,这就天然构成一个Stackelberg博弈,也就是主从博弈。主从博弈的好处是逻辑清晰,领导者(运营商)拥有先动优势,但同时必须考虑跟随者(用户)的理性响应曲线,找到能让自己收益最大化的定价方案。相比集中式优化,主从博弈更贴近实际的多主体交易场景,也更符合当前电力市场改革后"售电公司+终端用户"的竞争格局。
从科研角度看,这类模型的好处也显而易见:可以写出双层优化形式,上层是运营商的收益最大化,下层是用户的用能成本最小化,整个问题有明确的KKT条件可以去处理,数学上非常成熟,审稿人也买账。
1.2 需求响应与电能交互在博弈模型中的定位
这个题目里有两个"交互"值得单独强调。第一个是需求响应,它连接的是运营商的价格策略和用户的用能行为;第二个是电能交互,它连接的是多个用能主体之间的功率流动。
很多人做多主体优化时把需求响应看成负荷约束就完事了,但真正的博弈模型里,需求响应是下层用户问题的一部分。运营商定价之后,用户的负荷曲线、储能充放电策略、甚至可转移负荷的启停时间都会跟着变,变完之后又反过来影响运营商的收益和系统供需平衡。所以需求和响应不是一条静态曲线,而是整个博弈的动态反馈环节。
电能交互则是多主体场景特有的耦合。多个用户/微电网/能源站之间,可以通过公共联络线进行电能买卖。交互功率的大小和方向受制于线路容量和运行约束,而且交互电价同样可以由运营商制定或通过市场机制形成。加入电能交互的收益在于:系统可以从"各自为政"变成"互联互济",高负荷时段能够互相支援,整体运行成本自然下降。
再往下拆,这两块内容就会直接反映在数学模型的变量和约束中,具体怎么建,我在下一节详细展开。
2. 核心数学模型拆解
2.1 上层领导者:综合能源运营商的调度模型
我先给出运营商的典型设备构成。大多数学术文章和实际工程里的IES运营商,主要设备包括:CHP(热电联产机组)、燃气锅炉、电锅炉、储能电池、蓄热罐,还可能配一部分分布式光伏和风机。运营商的目标函数一般是自己的净收益最大化,常见形式是:
max F_leader = Σ(售电收入 + 售热收入 + 售气收入 + 电能交互收入) - Σ(购电成本 + 购气成本 + 运行维护成本 + 储能折旧成本)其中售电/售热/售气收入是用户侧交易得到的,电能交互收入是与外部配电网交易得到的。这个目标的本质是"卖出去的钱减去买进来的钱和运行损耗的成本",非常贴合售电公司的实际运营逻辑。
约束条件要从多个维度搭:
- 能量平衡约束:任意时段,电力均满足"CHP发电 + PV输出 + 储能放电 + 外购电力 + 交互功率 = 用户电负荷 + 电锅炉用电 + 储能充电"。热力平衡同理,CHP余热 + 燃气锅炉 + 蓄热罐放热 = 热负荷 + 蓄热罐蓄热。
- 设备运行约束:CHP存在电出力上下限、爬坡速率约束,同时热电比耦合;燃气锅炉有热出力上下限;储能电池有SOC递推方程和充放功率限制,而且要避免同时充放电。
- 交互功率约束:与配电网的购售电功率有上限,且为了安全考虑,通常不允许在同一时段既买入又卖出。
这些约束用Matlab+YALMIP表达并不复杂,最需要注意的是储能SOC的递推约束要写对时序,以及CHP的热电耦合约束如果引入二进制变量,问题规模会翻倍,要有心理准备。
2.2 跟随者:用户侧需求响应模型
下层用户的目标函数是自身用能成本最小,约束条件则体现"需求响应能力"。在实际代码中,常见的用户侧模型分三类,我在实现时建议分开写清楚,后面Debug会方便很多。
第一类:价格型需求响应(Price-based DR)
这一类基于需求价格弹性矩阵。核心思路是:用户在电价升高时降低用电需求,电价降低时增加用电需求。写成约束时,负荷变化量与电价变化量之间的关系为:
ΔL_i = E_ii * (Δp_i / p_i0) * L_i0 + Σ(E_ij * (Δp_j / p_j0) * L_i0)其中E_ii是自弹性系数,E_ij是交叉弹性系数。弹性矩阵一般由统计数据或实测数据得到,代码里可以直接写成常数矩阵,然后作为参数传入。
需要注意,价格型需求响应一般只改变用户的用电量曲线形状,不保证总用电量不变,所以如果做"电费中性"假设(无差异曲线分析),需要额外加一个总用电量守恒约束。这一点很多初学的同行容易漏。
第二类:激励型需求响应(Incentive-based DR)
这类需求响应通常指可中断负荷(IL)和直接负荷控制(DLC)。运营商与用户签订协议,在系统高峰时段可以切除一部分负荷,同时要支付相应的补偿费用。建模时引入二进制变量表示负荷是否被中断,并加中断次数限制。最终在下层目标函数中,用户会权衡削减负荷拿到的补偿与舒适度损失,这个平衡其实靠价格和补偿系数调节。
第三类:可转移负荷(Shiftable Load)
典型场景是电动汽车充电、洗衣机和消毒柜这类可延时负荷。约束条件为:负荷必须在一个允许的时间窗口内完成,并且有连续运行要求。建模时对每个时间窗口设置开始时间变量,用二进制变量表示"t时段是否启动"。这类约束的线性化形式非常成熟,代码写起来就是用big-M方法。
我个人习惯把三类需求响应拆成三个函数文件——calc_pdr.m、calc_idr.m、calc_slr.m,输入是电价序列和原始负荷参数,输出是响应后的负荷序列。这样方便单独调试,也可以灵活决定算例里到底激活哪几类需求响应。
2.3 电能交互与耦合约束
电能交互是多主体问题最核心的耦合部分,也是最容易在编程时出bug的地方。
先明确主体的边界。假设系统中有N个用户主体和一个运营商主体。用户之间通过公共母线建立联系,运营商作为中间调控方组织交互功率。电能交互的建模通常用以下几条约束:
- 交互功率限值约束:任意两个主体i和j之间的交互功率P_ex_ij(t)不能超过联络线允许容量P_ex_max。
- 交互平衡约束:总交互功率的代数和为零,即ΣP_ex_ij(t) = 0。这是保证多主体之间电能守恒的关键。
- 交互功率方向约束:若引入二进制变量区分买入/卖出状态,就需要加入互斥约束,避免同一时段既买入又卖出(这既不合常理也容易导致求解无解)。
- 交互价格约束:如果交互电价由运营商制定,价格要在购电成本和售电价格之间,以保证运营商有利可图且用户不会亏本买卖。通常设为购电成本的1.1~1.3倍。
从编程角度说,交互变量是N×T维矩阵,维度不大但会带来强烈的耦合性。如果问题是集中式求解,这些约束直接堆到总约束里就行;如果走分布式求解,则要把交互功率看成边界变量,通常会在目标函数里加一个二次惩罚项来加速收敛。
2.4 主从博弈求解的两条技术路线
这道题看着复杂,实际上业界主流的求解路线分两种,我把各自的特点讲透。
路线一:KKT条件+强对偶定理(MPEC方法)
思路很简单:下层问题是用户的最小化问题,如果它是线性规划(LP)或凸二次规划(QP),就可以写出它的KKT条件,然后用KKT条件替换下层优化问题,将双层问题转化为单层MPEC(带均衡约束的数学规划)。但此时目标函数中出现"电价×负荷"这类双线性项,需要借助强对偶定理或者利用KKT互补条件做线性化,最终转化为混合整数线性规划(MILP),调用Cplex/Gurobi求解。
这条路线胜在求解效率高、最优性有保证,学术文章里用得最多。缺点是推导过程繁琐,涉及大量补充松弛变量,手推很容易出错。我的经验是用Matlab的符号工具箱做推导辅助,或者先用小规模样例验证KKT条件正确性,再扩展到完整算例。
路线二:分布式迭代方法(启发式+嵌套优化)
这条路线是直接把博弈写成迭代形式:外层用启发式算法(粒子群、遗传算法、差分进化)搜索上层变量(价格、交互电价),内层用Cplex/Yalmip求解下层用户的优化问题,然后把用户的响应结果反馈到上层,计算上层目标函数。迭代收敛到的平衡点就是Stackelberg均衡。
这条路线好写好理解,但收敛性会让人头疼,尤其是粒子群参数没调好时,价格迭代曲线经常震荡。后面第4章我会详细讲我怎么处理这个问题的。总体建议:如果对求解精度有硬要求或者被审稿人质问"是否全局最优",尽量走KKT路线;如果只是为了快速搭Demo、验证策略有效性,分布式迭代够用。
3. Matlab仿真实现全流程
3.1 代码总体框架
一个标准的主从博弈综合能源系统Matlab工程,我推荐按以下目录组织:
IES_Stackelberg/ ├── main.m ├── data/ │ ├── load_data.m % 负荷与可再生能源曲线 │ ├── price_data.m % 分时电价、气价参数 │ └── system_params.m % 设备参数 ├── models/ │ ├── upper_leader.m % 上层运营商优化模型 │ ├── lower_user.m % 下层用户需求响应模型 │ └── interaction.m % 电能交互与耦合约束 ├── solvers/ │ ├── solve_kkt.m % KKT单层化求解 │ └── solve_iter.m % 分布式迭代求解 └── utils/ ├── plot_results.m % 绘图 └── check_converge.m% 收敛性检测main.m是整个程序的主入口,统一负责数据加载、模型组装、求解和结果输出。把数据和模型分离是很多Matlab新手容易忽略的一点,实际项目一跑起来你会发现,改参数、换数据是常态,不分离的后果就是改一个参数要翻半天代码。
3.2 参数定义与数据初始化
我在代码里直接用结构体管理参数,这比散落的变量清晰得多。以机组和储能为例:
%% 系统参数设置 devices = struct(); devices.CHP.e_min = 100; % CHP电出力下限,kW devices.CHP.e_max = 800; % CHP电出力上限,kW devices.CHP.hpr = 1.3; % 热电比,实际项目中可取0.9~1.5 devices.CHP.eta_e = 0.35; % 发电效率 devices.CHP.eta_h = 0.45; % 供热效率 devices.CHP.ramp = 200; % 爬坡速率,kW/h devices.GB.h_min = 0; devices.GB.h_max = 500; % 燃气锅炉热出力上限,kW devices.GB.eta = 0.9; % 热效率 devices.ES.cap = 1000; % 储能电池容量,kWh devices.ES.p_max = 200; % 最大充放电功率,kW devices.ES.eta_ch = 0.95; % 充电效率 devices.ES.eta_dis = 0.95; % 放电效率 devices.ES.soc_min = 0.1; % SOC下限 devices.ES.soc_max = 0.9; % SOC上限 devices.ES.soc_0 = 0.2; % 初始SOC时间尺度方面,我用的标准调度周期是24小时,步长为1小时,这是这个领域最常见的配置。如果你做的是日内滚动优化,也可以改成96个时段,每15分钟一个点,这时候变量维度会直接乘以4,求解时间呈指数上涨,要有心理准备。
3.3 需求响应模型的编码实例
以价格型需求响应为例,这里我直接展示核心代码编写思路。弹性矩阵可以直接从外部数据文件读入:
function L_new = price_dr(L0, p, p_ref, E) % 输入: % L0 - 基准负荷,1×T向量 % p - 实际电价,1×T向量 % p_ref- 参考电价(基准情景电价),1×T向量 % E - 需求价格弹性矩阵,T×T % 输出: % L_new- 需求响应后的负荷,1×T向量 T = length(L0); delta_p_ratio = (p - p_ref) ./ p_ref; % 电价变化率向量 L_new = L0; for i = 1:T flexible = 0; for j = 1:T flexible = flexible + E(i,j) * delta_p_ratio(j) * L0(i); end L_new(i) = L0(i) * (1 + flexible); end % 可选:总用电量守恒约束修正,保证用户总用电量不变 % L_new = L_new * sum(L0) / sum(L_new); end这里有个很容易踩的细节:弹性矩阵E的主对角线元素(自弹性)通常为负值,交叉弹性(非对角线元素)通常为正值,这代表"自身电价上升削减自身负荷,其他时段电价上升时转移负荷到本时段"。如果你写反了符号,那响应出来的负荷曲线会完全反直觉,甚至会放大峰谷差。
可中断负荷(激励型DR)的代码,我用YALMIP的binvar来定义中断状态变量:
T = 24; I_il = binvar(1, T); % 1表示负荷被中断 P_il_max = 50; % 每个时段可中断负荷上限(kW) N_il_max = 4; % 最多中断的时段数 Constraints = [Constraints, 0 <= P_il <= I_il * P_il_max]; Constraints = [Constraints, sum(I_il) <= N_il_max];注意你有没有想过为什么要限制N_il_max?如果运营商给的中断补偿足够高,用户恨不得每个时段都被拉闸,启动次数限制本质上是模拟用户舒适度和设备寿命的现实约束,不做限制的话模型过于理想化。
3.4 博弈迭代主循环实现
分布式迭代是理解整套博弈计算流程的最好入口,代码逻辑也很直白,我贴一个框架:
% 初始化 iter = 0; lambda = 0.5 * (c_buy + s_sell); % 初始电价取购售电价的中间值 alpha = 0.3; % 价格更新步长(需要调参) tol = 1e-4; max_iter = 30; err = inf; while err > tol && iter < max_iter iter = iter + 1; % 第一步:已知运营商电价,求解下层用户问题 [L_user, cost_user] = lower_user_solve(lambda, price_data, device_data); % 第二步:将用户响应结果传回上层,求解运营商优化问题 [P_operator, revenue_operator] = upper_leader_solve(L_user, price_data, device_data); % 第三步:更新价格(这里用最经典的迭代公式) lambda_new = lambda + alpha * (L_user - L_target); % 第四步:检测收敛性 err = max(abs(lambda_new - lambda)); lambda = lambda_new; % 记录每一轮的结果,方便画收敛曲线 history.lambda(iter, :) = lambda; history.revenue(iter) = revenue_operator; history.cost(iter) = cost_user; end这里最核心也最玄学的部分就是第三步的价格更新。我一开始用固定步长时,价格迭代曲线像心电图一样疯狂震荡,后来换了阻尼步长,效果立竿见影。所谓阻尼步长就是随着迭代次数增加逐渐缩小更新幅度,相当于每次价格调整得越来越"谨慎":
alpha = alpha0 / sqrt(iter); % alpha0为初始步长再进阶一点的做法是用平均值法(每次取前几轮的平均值作为新价格),或者借鉴次梯度类算法的平滑技巧。具体怎么选,往往要看你的电价目标曲线是什么形状,这个没有通用的万能答案,建议多看几个典型场景的收敛曲线再定。
3.5 结果绘图与分析
Matlab出图是整套代码最后也是最能直观体现成果的环节。我一般画这么几张图:
第一张是系统电功率平衡图。用堆叠面积图展示CHP、光伏、储能放电、外购电、交互功率随时间的变化,直观检验任何时候功率都是平衡的,这个图在写论文、做汇报时是必备的。
第二张是价格与负荷的响应曲线对比图。把无需求响应时的负荷曲线和有需求响应时的负荷曲线画在一起,叠加分时电价作为右侧坐标轴,一眼就能看出需求响应是否削峰填谷、用户是否做到合理避峰。
第三张是储能SOC和充放电功率图。储能调度策略是否合理,看这个图最直观,SOC曲线应该是平滑过渡的,不能有突兀跳变。如果SOC从0.1瞬间跳到0.9,说明程序里的充放电功率约束或者时序关系写错了。
第四张是博弈收敛曲线。把每一轮迭代的运营商收益或电价画出来,呈现单调收敛趋势,这是审稿人和答辩老师最喜欢看到的一张图,也是验证模型求解可靠性的直接证据。
4. 频率踩坑记录与排查技巧
4.1 迭代不收敛:价格震荡的真相
分布式迭代最常见的翻车现场就是价格震荡不收敛。我踩过最深的一次坑,是在做多主体电能交互时,直接把交互功率增量乘以固定步长叠加到电价上,结果50次迭代下来,电价不是收敛到均衡,而是呈现周期为2的振荡——一会儿极度抬高、一会儿极度压低,完全没法用。
后来我梳理清楚原因了:固定步长太大时,价格更新会"越过"均衡点;步长太小时又收敛得太慢。而且多主体交互让响应函数带有一定的"惯性",价格更新必须考虑前一时刻的交互功率方向。
我的解决办法分两步。第一步,改步长策略,采用衰减步长方案,让前期快速逼近、后期精细收敛。第二步,加低通滤波,即每次更新的价格取"当前迭代计算值"和"上一轮价格"的加权平均,权重可以取0.3/0.7,效果立竿见影。
lambda_new = 0.3 * lambda_calc + 0.7 * lambda_prev;有人管这叫"价格平滑",本质上就是抑制高频振荡,思路跟控制理论里的阻尼器差不多。
4.2 求解器报无可行解的排查清单
无论你用的是Yalmip+Cplex还是Gurobi,遇到"infeasible problem"都是家常便饭。我总结了一套排查顺序,能解决90%的问题:
第一优先级查等式约束。能量守恒约束其实最容易被写错,比如储能充放电同时发生的隐性bug会导致等式约束永远无法满足,因为同一个时刻既要充电又要放电,超出物理可能。这时候给储能加一个"充放互斥"约束logical constraint或二进制变量就能解决。
第二优先级查SOC初始条件。储能初始SOC设为0.2,但终态SOC要求等于0.2时,如果系统净电量不足或者充放电效率损失被忽略,很容易导致无解。建议先去掉SOC终值约束跑一遍,如果解出来了,说明是能量不平衡问题,再调设备参数或者加惩罚项。
第三优先级查交互功率约束。多主体同时买卖会导致功率平衡约束被破坏。一个快速定位方法是把交互约束全部注释掉,如果问题可解,就逐渐加上限制,二分定位到出问题的约束。
4.3 运行时间爆炸的处理思路
综合能源系统的维度一旦上去,Cplex求解时间很容易从秒变成小时。我实测过一个24节点、24时段的算例,直接求解MILP用了近40分钟,这在参数调试阶段根本无法接受。
优化手段从易到难排列:如果上层变量是多维连续变量,先用线性规划松弛版本测速;大量使用big-M时,M值不宜取得过大,过大会让松弛的可行域过大,求解器分支定界效率暴跌,工程上取设备最大功率的2倍左右足够;如果可以接受精度损失,将原问题分解为多个子问题依次求解,用热启动参数传入初始解,Cplex能大幅减少分支切割的工作量。
4.4 结果的合理性自查
代码能跑出结果,不等于结果是正确的。我每次跑完程序至少会做三件事:
第一,检查设备出力是否在上下限边界内,重点看CHP和GB有没有超过容量输出。如果某设备长期运行在最大值而系统仍然满足不了负荷,那说明设备容量配置不合理,或者需求响应参数设置太保守。
第二,检查储能SOC曲线是否符合物理规律。SOC变化趋势应该与充放电功率曲线一致,充多放少则SOC上升,反之下降。如果SOC曲线毛刺很多,大概率是时序索引写错了,比如用t和t-1时出了边界。
第三,核对总账。把运营商的总收入、总成本、净收益分别打印出来,检查利润是否合理。如果运营商亏本运行,大概率是交互电价或售能价格设置不合理;如果用户成本比基准情景还高,那需求响应就失去意义了。
5. 实操经验与个人心得
这套主从博弈优化调度模型,我从写第一个简陋单用户版本到现在加入多主体交互,前后折腾了大半年。最大的体会是:数学模型写出来再漂亮,落到Matlab代码上都是从一个个细节堆起来的。弹性矩阵的正负号、SOC递推的索引、价格迭代的步长,任何一个地方出错,结果看起来都会跟"合理"差那么一点,但又很难一眼看出错在哪。
这里分享一个我后来固定下来的调试习惯:先跑一个最小的2时段、2主体诱导弹例,把每一个约束的残差打印出来,手算一遍验证结果完全对得上,再扩展到完整24时段算例。虽然前期多花一小时,但后面Debug省出来的时间可能是一天。
再有一个建议是:主从博弈模型的验证不能只看最终目标函数值,一定要画迭代过程的收敛曲线。收敛曲线既证明了算法有效性,也方便观察是否存在局部振荡。那些看着"数字合理"但收敛曲线乱跳的结果,在答辩和审稿阶段很容易被打回来。
如果后续你想在这个方向上继续扩展,可以考虑把单层博弈拓展为主从-对等混合博弈,即运营商与用户之间是主从关系,但多个用户之间是对等关系,需要同时求解Stackelberg均衡和Nash均衡,这也是现在比较热门的交叉方向。模型复杂度会提升一个量级,求解方法也需要调整,但底层这套Matlab框架完全可以复用,变量定义和约束组织方式不需要推倒重来。