1. 项目概述与整体思路
这几年做综合能源系统优化,大量论文都在用主从博弈,但真正的落地代码细节其实很少公开。这个项目解决的核心问题很直接:综合能源微网(电、热、气多能耦合)内部有多个利益主体,每个主体都有自己的用能成本和舒适度诉求,而共享储能作为第三方投资者,既要考虑自身投资收益,又要兼顾微网用户的用能利益。如果全盘集中式优化,相当于让储能运营商和用户"合并成一家",现实中根本行不通——储能是谁建的?钱谁出?收益怎么分?
主从博弈(Stackelberg Game)恰好是处理这种层级决策关系的自然框架。储能运营商作为领导者(Leader)先制定充放电价,微网用户作为追随者(Follower)基于价格信号优化自己的用能计划。领导者预判追随者的响应行为来最大化自身收益,追随者则在给定价格下追求自身成本最小。这个"先决策-后响应-再迭代"的结构,跟现实中区域能源市场的交易机制高度吻合。项目用MATLAB完整实现了这套博弈流程,包含双层优化模型的构建、KKT条件转换、强对偶松弛,以及迭代求解全过程。
整套代码适合三类人:一是做微电网/综合能源方向的研究生,需要复现博弈模型跑仿真的;二是做共享储能商业模式设计的工程师,想验证定价策略对用户侧响应的影响;三是刚接触双层优化的同学,想看懂KKT转换和求解器调用的完整套路。配置环境用MATLAB R2020b以上加YALMIP工具箱,求解器用CPLEX或Gurobi都可以,代码里做了接口兼容处理。
2. 主从博弈模型的角色设计与经济逻辑
2.1 为什么要分"领导-跟随"两级
传统单层优化把储能和微网看成同一个决策主体,得到一个全局最优解,但这个解在现实中基本不可执行。储能运营商不会牺牲自己的收益去补贴用户,用户也不会完全听从储能的调度指令。主从博弈的建模思想是:承认各主体目标不一致,但在层级结构中达成均衡。
具体到本项目,上层领导者是共享储能运营商,决策变量是储能各时段的充放电价,目标函数是自己在一个调度周期内的净收益最大化,包含售电收入、购电成本、储能折旧成本三部分。下层追随者是多个综合能源微网用户,每个微网收到储能公布的充放电价后,优化自己的电、热、气购能计划以及用户侧灵活性资源的调度,目标函数是自己一天的总用能成本最小。
这里有一个容易被忽视的经济逻辑:储能的购电价格和售电价格不是对称的——储能向微网售电的价格应该高于储能从电网购电的价格,否则储能无法回收成本;但价格又不能过高,否则微网用户会全部转向电网购电,储能反而无利可图。所以价格带的设置要卡在电网购电价和微网内部发电成本之间的区间,这个区间越宽,博弈的可行域越大。
2.2 共享储能的收益结构与定价机制
共享储能的收益来源主要有三块:峰谷套利、向微网售电的服务费、参与辅助服务市场的潜在收益。本项目模型把前两块纳入优化,第三块作为可扩展接口留出。
定价机制采用分时电价引导策略,分为充电价格和放电价格两组决策变量,每组覆盖24个时段。为了保持博弈的合理性,代码里加入了两个约束:一天内储能售电收入的期望值必须大于充电成本加折旧成本,否则储能退出市场;同时各时段售电价格不能超过电网购电价的上限,否则微网完全可以从电网买电而不理储能。
这个设计思路很关键。很多新手把双层模型写出来以后,发现下层优化结果对价格变动的响应要么过于敏感、要么完全迟钝,问题往往出在下层微网的负荷弹性设置上。本项目给每个微网配置了可转移负荷、可中断负荷和储热罐三类柔性资源,价格升高时用户会转移负荷时段、削减可中断负荷,价格低谷时则多购电储热,这样下层微网对价格的响应曲线就是平滑且符合实际的。
2.3 下层微网的多能互补模型
综合能源微网的特点是多能耦合:电锅炉、燃气轮机、吸收式制冷机、储热罐、光伏等多个单元之间相互约束。电负荷由光伏、燃气轮机、储能放电和电网购电共同满足;热负荷由燃气轮机余热回收、电锅炉和储热罐放热共同满足;冷负荷由吸收式制冷机和电制冷机共同满足。
下层微网的优化目标不只是购电成本,还包括购气成本、设备运行维护成本、以及可中断负荷的补偿费用。约束条件里包含能量平衡约束、设备出力上下限约束、爬坡约束、储热罐的容量约束和充放热速率约束。
这里有一个多能流模型的细节:燃气轮机的热电比决定了热出力与电出力之间的强耦合,代码里用线性化的热电运行区间来描述,避免了非线性模型导致的求解困难。光伏出力则按典型日曲线输入,属于不可调度的间歇性电源,在模型里表现为负的负荷项。
3. 双层优化求解:KKT转换与求解器衔接
3.1 双层模型与单层化的数学推导
主从博弈模型的直接求解思路是迭代法:上层给定价格,下层求解用户响应,上层根据用户响应调整价格,循环迭代直到收敛。这种"串行迭代"的思路实现简单,但存在两个问题:一是收敛性没有理论保证,价格和响应可能陷入振荡;二是每次迭代都要调用两次求解器,计算开销大。
更严谨的做法是利用KKT条件将下层优化问题转换为上层优化问题的约束条件,把双层优化变成单层混合整数规划。具体操作是:对下层微网优化问题写出拉格朗日函数,对各决策变量求偏导并令其为0,得到KKT条件;再引入互补松弛约束的线性化表达——利用大M法将互补条件转换为带二进制变量的线性不等式约束。
代码里涉及的互补松弛条件数量比较多,因为下层微网包含多个设备约束和不等式约束。实现时需要注意大M值的选取:M太小会切掉真实可行解,M太大会导致数值病态。经验上是根据所有决策变量的量级,选取比最大可能值大1个数量级的数值,一般取1e4到1e5之间。这个细节直接决定求解器能不能收敛,我在调试过程中吃过不少亏。
3.2 强对偶条件的适时把握
处理下层优化问题时,如果目标函数是凸的、约束是线性的(本项目下层模型符合这个条件),可以利用强对偶定理进行等价变换。强对偶条件的核心是:将下层目标函数中的双线性项(价格变量乘以下层决策变量)用其对偶变量与约束参数的形式替换掉,从而消除非线性项。
这个操作的意义很大。原来的单层化模型中,上层价格变量和下层功率变量相乘形成非线性项,对求解器极不友好。经过强对偶变换后,目标函数变成纯线性表达式,整个模型转化为混合整数线性规划(MILP),CPLEX和Gurobi求解效率极高。
但强对偶的使用有一个前提要求:下层优化问题必须满足Slater条件,即存在使所有不等式约束严格成立的可行解。对于微网优化问题,这个条件通常满足,不需要额外处理。如果遇到非凸的下层模型(比如包含二进制变量的机组启停),就需要改用MPEC或启发式算法求解,那计算复杂度会上一个台阶。
3.3 求解流程与参数配置
整体求解流程分三步:模型初始化、单层化转换、求解与结果后处理。
第一步,读入基础数据,包括各微网的负荷曲线、光伏出力预测曲线、电网分时电价、天然气价格、储能参数、设备参数。注意所有参数都要统一单位(电功率用kW,热功率用kW,能量用kWh,价格用元/kWh),这是很多新手容易踩坑的地方,单位不统一会导致约束矩阵的条件数恶化。
第二步,把下层微网模型的KKT条件、强对偶等式和互补松弛线性化约束全部拼接到上层模型中,形成完整的MILP问题。这一步代码量大但逻辑清晰,只需要按公式逐条写约束即可。
第三步,调用YALMIP的optimize函数求解,输出储能最优定价策略、各微网用能计划、储能充放电曲线和各方收益。代码中设置了gap阈值作为收敛判断指标(默认1e-4),用于迭代法的对比验证。
实际调用CPLEX求解MILP时,有两个参数需要特别关注。一是CPLEX的mip.tolerances.mipgap设置为0.01还是0.001,影响求解速度与精度的平衡;二是timelimit设置,防止模型因为复杂度太高长时间跑不出来。本项目模型的变量规模在几千个连续变量加几百个二进制变量的量级,CPLEX通常能在几十秒内求解,不需要额外优化。
4. MATLAB代码实现:从零搭建完整求解框架
4.1 主程序结构设计
整个MATLAB项目采用模块化结构,共分为:主程序入口(main.m)、数据读取模块(load_data.m)、模型构建模块(:上层模型函数build_upper_model.m、下层KKT转换模块build_lower_kkt.m)、求解模块(solve_optimization.m)和结果分析模块(plot_results.m)。
主程序入口的流程如下:加载基础数据、初始化结构体变量、调用模型构建函数生成约束与目标、调用求解器、解析结果并绘图。结构体变量贯穿全局,包含param结构体(存储系统参数)、data结构体(存储负荷和光伏数据)、model结构体(存储决策变量和约束句柄)、result结构体(存储求解结果)。
这样的结构设计给调试带来极大便利。某条约束结构异常,可以直接在对应模块函数中设置断点检查,不必在主程序中翻找。另外模块化结构也方便替换数据文件进行多场景仿真,比如修改负荷曲线参数、改变储能容量规模、调整价格上限,都只需要修改数据文件而不用改动核心求解代码。
4.2 决策变量定义与约束构建的核心代码模式
以下给出上层模型决策变量定义的典型代码段,这个模式是整个项目中反复使用的基础模板:
%% 决策变量定义示例 % 上层:储能运营商定价策略 P_ch_price = sdpvar(24, 1, 'full'); % 各时段储能充电价格 P_dis_price = sdpvar(24, 1, 'full'); % 各时段储能放电价格 % 下层:微网用能计划(每个微网一套) % 以微网1为例 P_grid = sdpvar(24, 1, 'full'); % 电网购电功率 P_gt = sdpvar(24, 1, 'full'); % 燃气轮机发电功率 Q_gt = sdpvar(24, 1, 'full'); % 燃气轮机余热回收热功率 P_eb = sdpvar(24, 1, 'full'); % 电锅炉耗电功率 SOC = sdpvar(24, 1, 'full'); % 储热罐储热状态 P_es_dis = sdpvar(24, 1, 'full'); % 储能放电量(购自储能) P_es_ch = sdpvar(24, 1, 'full'); % 储能充电量(售给储能)约束构建时,能量平衡约束的写法很直接,但要注意时序循环的索引处理。储能SOC递推约束在首时段和后续时段有不同的表达方式,代码中通过if判断区分首段逻辑。
互补松弛条件的大M线性化则采用以下模板,这是整个单层化转换中最容易出错的环节之一:
%% 互补松弛条件线性化(以储能充放电量约束为例) % 原约束: 0 <= P_es_dis <= P_es_dis_max % 对偶变量: lambda_dis_lower >= 0, lambda_dis_upper >= 0 % 互补条件: P_es_dis * lambda_dis_lower = 0 % (P_es_dis_max - P_es_dis) * lambda_dis_upper = 0 % 引入二进制变量z1, z2 z1 = binvar(24, 1); z2 = binvar(24, 1); % 大M线性化 Constraints = [Constraints, ... P_es_dis <= M * z1, ... lambda_dis_lower <= M * (1 - z1), ... P_es_dis_max - P_es_dis <= M * z2, ... lambda_dis_upper <= M * (1 - z2)];4.3 YALMIP与CPLEX的接口配置细节
项目中所有优化模型都用YALMIP建模,底层求解器用CPLEX。YALMIP的最大优势是建模语言简洁,约束和目标的表达接近数学公式,不需要手动处理求解器的API调用。
安装YALMIP后还需要正确配置CPLEX接口。一种简单可靠的配置方式是在MATLAB中添加到路径,但注意CPLEX的版本和MATLAB版本兼容性:CPLEX 12.10对应MATLAB R2020b至R2023a,更高版本的MATLAB需要升级CPLEX版本,否则调用时会报"Unable to load cplexlink"错误。
调用求解器的代码模式如下:
%% 求解调用示例 options = sdpsettings('solver', 'cplex', ... 'verbose', 1, ... 'cplex.mip.tolerances.mipgap', 0.001, ... 'cplex.timelimit', 300); sol = optimize(Constraints, Objective, options); % 结果检查 if sol.problem == 0 disp('求解成功'); else disp(['求解失败: ', sol.info]); end如果机器上不方便安装CPLEX,Gurobi也是很好的替代方案,YALMIP接口写法几乎不变,只需要把solver参数改为'gurobi'。Gurobi的许可证获取更方便,学术版申请即可,而且商用版的混合整数规划求解性能在多数场景下与CPLEX相当。
4.4 结果可视化与分析
仿真结果的可视化直接决定了论文插图的质量。代码中提供了三套绘图模板:第一套是储能充放电功率与SOC的时序图,展示储能运行状态;第二套是各微网的电功率平衡图(堆叠柱状图形式,展示光伏、燃气轮机、电网购电、储能放电的功率构成);第三套是博弈迭代过程中双方收益的收敛曲线图。
绘图时有一个经验:如果结果图中出现负功率(比如燃气轮机出力为负值、储能SOC突变),不要急着怀疑求解器,优先检查单位转换和数据读取的正确性。负荷数据通常以kW为单位,而储能容量参数可能以kWh为单位,计算SOC递推时两者必须换算成一致的时间粒度和功率单位。
5. 常见问题排查与实操经验
5.1 模型无解与求解器报错的高频原因
第一个高频问题是模型无解。遇到这个情况,先检查大M值是否过大导致数值病态,其次检查互补松弛条件的二进制变量数量是否与原约束一一对应,漏掉任何一个互补条件都会导致约束集不一致。
第二个高频问题是目标函数出现非预期量级。当储能价格变量乘以微网功率变量的数量级很大(比如价格100元/kWh、功率500kW),目标函数动辄上万,此时约束的容差相对不敏感,但数值条件变差。解决思路是在建模前对变量进行标幺化处理,将所有功率除以基准值(比如100kW),价格除以基准价格,保持所有变量同一数量级。
第三个高频问题是求解结果中储能价格始终等于价格上限或下限。这通常是模型退化的表现,说明博弈均衡退化成了边界解。处理方法有两种:一是调整储能折旧成本系数,让边际成本回归合理范围;二是放宽价格带的上下限,给博弈留出决策空间。
5.2 迭代法vs单层化法的结果对比验证
项目中同时实现了迭代求解法和KKT单层化求解法,用于互相验证。迭代法的时间开销通常很大,需要几十次上下层交替求解才能收敛;而单层化方法只需要一次MILP求解。两者的结果应该非常接近,如果出现显著差异(比如储能收益偏差超过5%),基本可以判断是单层化转换中落了约束,或者迭代法未收敛就提前截止。
我自己在调试一个多微网扩展版本时,就遇到过迭代法结果和单层化结果不一致的情况。排查了两个小时后发现,是下层微网的储热罐SOC递推约束在KKT转换时漏掉了末时段约束,导致下层模型单层化后出现了一个不合理的高储热状态。所以对比较复杂的模型,建议先跑通单微网场景,验证代码逻辑后再扩展到多微网。
5.3 经济参数的灵敏度分析思路
模型跑通之后,可以做三类灵敏度分析来增强论文的说服力。第一类是储能容量对系统运行的影响:逐渐增加共享储能容量,观察微网总用能成本的下降幅度和储能收益的变化趋势,通常会出现边际收益递减的拐点。第二类是电网分时电价峰谷差的影响:峰谷差扩大时,储能的套利空间增大,蓄意充放电行为更积极。第三类是天然气价格对多能互补的影响:气价波动时,燃气轮机的出力策略和电锅炉的替代效应会发生明显变化。
做灵敏度分析时建议批量生成仿真脚本,用for循环跑完所有场景,再把结果汇总成对比表格。这里有一个实战技巧:MATLAB的parfor并行计算非常适合这类多场景循环任务,把不同参数场景分配到多个worker同时求解,能大幅压缩仿真时间。
5.4 模型扩展方向与代码预留接口
当前模型已经把共享储能定位为单一博弈领导者,实际中可能有多家储能运营商竞争,模型可以扩展为多个领导者的斯坦伯格博弈,复杂度会显著上升。也可以把下层微网从被动响应者扩展为主动参与市场交易的主体,这样博弈结构会变成双层双向互动,这对代码架构的扩展性要求更高。
项目中预留了数据接口和模型接口:数据接口支持修改微网数量(从1个扩展到5个)、替换光伏预测数据、修改负荷特性参数;模型接口支持新增设备类型(比如加入电解制氢装置、碳捕集装置),只需在设备参数结构和约束构建函数中增加对应模块即可,主程序流程不需要改动。
在实际使用中,这套代码还配合过机器学习方法做场景预测——用历史数据训练光伏出力和负荷预测模型,再把预测结果输入博弈模型,实现日前调度和日内滚动优化的衔接。这个方向也是目前综合能源系统研究的热点之一,值得在现有基础上继续探索。
最后说一个实操心得:双层博弈优化的代码调试,瓶颈常常不在求解器,而在模型设计的合理性上。价格变量的取值范围、负荷弹性的大小、储能成本参数的设定,这些经济参数才是决定结果是否符合实际的关键。在跑通代码之后,我建议你先用一组"常识数据"验证结果的合理性——比如储能显然应该在电价高峰放电、低谷充电,微网显然应该在气价低时多发电、气价高时多购电——如果这些直觉性的结果都验证不通过,再去查模型代码的bug,效率会高很多。