“装了光伏又装了储能,白天发电用不完,晚上想再买回来——这个账到底怎么算才公平?”这是我在某高校能源课题组交流时,一位研究生提出的问题。他在做的正是“基于分布式优化的多产消者非合作博弈能量共享模型及MATLAB代码复现”,简单说,就是让小区里的每一户人家(产消者)各自决定卖电或买电,不靠电网统一调度,也不靠某个“管理员”分配利益,而是通过各自的成本效益博弈,自发形成一个所有人都能接受的交易结果。这篇博文围绕这个课题,把博弈建模、分布式求解、MATLAB复现这条链路完整拆开,适合正在做能源交易、微电网调度、多智能体决策相关研究的学生和工程人员参考。
先说清楚这个模型到底解决什么问题。传统电力调度是集中式的,一个控制中心收集所有用户的数据,算出最优方案,再下发指令。但在多产消者场景下,每一户都有自己的光伏、储能、负荷,利益诉求各不相同:有的想多卖电赚钱,有的想低价买电省钱,有的宁可自己储能也不跟邻居交易。这种情况下集中式调度既涉及隐私问题(用户不愿意上报完整用电数据),又面临计算压力,更重要的是缺乏对个体利益诉求的尊重。非合作博弈的思想是:每个产消者都是一个独立的理性决策者,只追求自身效用最大化,彼此之间通过电价、交易量形成耦合,最终在纳什均衡点达到一种“谁单方面改变策略都无法获得更多收益”的稳定状态。
1. 产消者能量共享为什么要用博弈论:从“利益冲突”到“纳什均衡”
多产消者能量共享这个场景,天然具备博弈的三个要素:参与者(多个产消者)、策略(每个时段的购售电量与储能充放电计划)、收益(运行成本或效用函数)。但本文涉及的模型,目标函数通常不是简单的最小化成本,而是考虑用户对用能效用感知的非线性效用函数,比如常用的是二次效用函数,刻画用户对用电量和用电价格的主观满意度。
常见的建模方式是把每个产消者描述为一个追求自身净收益最大化的智能体,净收益包括三部分:用电带来的效用、与邻居交易电能的收益或成本、与电网交易的成本或收益。需要注意的是,产消者之间的交易电价不再由电网统一制定,而是由共享机制决定,最常见的是基于“内部电价”的设计——这个内部电价往往由所有产消者的综合供需情况内生决定,或者通过外部嵌套迭代更新。这种设计让每个用户的行为都会影响到内部电价,而内部电价又会反过来影响每个用户的决策,形成博弈闭环。
这里一个关键的误解是需要纠正的:非合作博弈并不等于完全对抗,它只是不要求所有参与者达成一个有约束力的合作协议。在能量共享中,每个产消者仍然可以从共享交易中获利(比如比直接卖给电网的价格更高),只是每个人在决策时只为自己的利益考虑,最终达到的均衡结果恰好能兼顾整体效益。这也是为什么这类模型在企业微电网、乡村社区能源自治、零碳园区等场景中被广泛讨论:不需要一个中心化的调度机构来分配利益,只需要设计好交易规则,各主体在规则下自行博弈,就能实现供需平衡和价值分配。
2. 模型构建的关键设计:决策变量、效用函数与约束条件的耦合逻辑
2.1 决策变量与目标函数的设计思路
在典型的“多产消者非合作博弈能量共享”模型中,每个产消者i在时段t的决策变量通常包含:与电网的购电量、与电网的售电量、与共享市场内其他产消者的净购电量(或净售电量)、储能充放电功率。我复现的参考模型中,为了简化并不失一般性,通常假设产消者直接与一个“共享市场”发生交易,而不逐一签订双边合约,这个市场形成一个统一的内部交易电价。
每个产消者的目标函数可以写成:
净收益 = 用电效用 - 从电网购电成本 + 向电网售电收益 +(内部市场交易收益或成本) - 储能运行成本
其中用电效用函数如果采用二次形式,一般写成:效用 = a·x - (b/2)·x²,不同产消者的a、b参数各不相同,反映出对不同时段用电的偏好差异。在实际实验中,a设置在0.5到1.5之间,b设置在0.01到0.05之间比较常见,取值跨度不要太小,否则均衡解会过于接近,看不出博弈特征。
储能约束需要特别小心:荷电状态(SOC)状态转移方程是线性等式的,但充放电功率与SOC上下限是双边不等式约束,此外还要考虑储能不能在同一个时段同时充放电——这一条我见过不少复现代码直接用充放电功率相乘为0来处理,导致模型变成非凸问题。更稳妥的做法是引入二进制变量来区分充放电状态,或者将目标函数中的储能退化成本设置为充放电的线性成本,在一定条件下自然避免同时充放电,比如在能量价格为正且储能充放存在损耗时,理性产消者就不会做“一边充电一边放电”的亏损操作。
2.2 供需平衡约束:耦合所有产消者的“纽带”
多产消者模型区别于单用户模型最核心的地方,就是一条全局的供需平衡约束:内部市场上所有产消者的净购电之和等于净售电之和,也就是市场出清条件。这条约束本身不归属于任何一个产消者,它的拉格朗日乘子恰好就是内部交易电价。这也是分布式优化求解中的一个关键抓手:如果用一个集中式协调者来协调所有产消者,那么只需要迭代调整这个乘子;如果采用完全分布式的博弈迭代,则每个产消者需要基于对内部电价的估计来更新自己的策略。
在实际建模中,还需要考虑一个物理细节:内部市场的交易电量通常不能超过配电网的物理容量限制,比如某条线路的传输功率上限。在基础模型中我建议先不加这个约束,等博弈均衡算法完全跑通、结果验证无误后再加入,否则分布式算法迭代初期很可能因为可行域过窄而难以收敛。
另外一个容易忽视的耦合点是:每个产消者与电网的交互功率往往受到接入容量的限制,比如同一台配电变压器容量有限,所有产消者从电网购电的功率之和不能超过某个上限。这种全局耦合约束的加入,会显著增加分布式求解的难度,需要采用额外的投影算法或扩展ADMM框架来应对。
2.3 效用函数参数如何影响均衡结果:一组对照实验
我在复现过程中做过一组参数对照实验,固定所有其他条件不变,只改变产消者A的效用参数,对比均衡结果的变化。
| 参数设置 | 产消者A的用电偏好 | 均衡时的内部电价 | 产消者A净购电量 | 产消者B净购电量 |
|---|---|---|---|---|
| 基准组 | a=1.0, b=0.02 | 0.42 | 12.5 | -5.2 |
| A偏好增强 | a=1.5, b=0.02 | 0.51 | 18.3 | -9.1 |
| A偏好减弱 | a=0.6, b=0.02 | 0.35 | 7.4 | -2.3 |
从结果可以清晰看出,产消者A用电偏好增强后,其购电需求上升,内部电价被推高,连带着改变了产消者B的售电策略——B在电价更高时更愿意多卖电。这正好体现了博弈的“策略互动”特性:一个参与者的参数变化不仅改变自身策略,还会通过均衡作用重塑所有参与者的决策。这也是为什么在做代码复现时,不能只检查单一参与者的收敛性,而是要同时观察所有参与者的策略序列和内部电价的演化轨迹。
3. 分布式优化求解:为什么用分布式,以及ADMM拆分的核心步骤
3.1 集中式与分布式,最终为什么选了分布式
直接写出所有产消者的目标函数之和,加上全局约束,交给求解器一次性求解,得到的是全局社会最优解,计算上没有任何障碍。但这里的核心问题是:全局建模需要所有产消者把自己的用电参数、储能参数、成本参数全部汇聚到中心节点,这在真实社区场景中几乎不可能实现——没有哪个家庭愿意把自己的用电曲线和储能策略全量上报给一个数据平台。分布式求解的价值就在于,每个产消者只需要保留自己的目标函数和约束,内部市场的全局约束通过迭代乘子来协调,最终收敛到同一个均衡解,但没有任何一方需要知道别人的完整隐私数据。
另一个更贴近实际的原因是:集中式求解得到的“社会最优”并不一定是每个个体愿意接受的方案。某个产消者可能为了全局最优要牺牲很大一部分自身利益,但没有激励机制保证TA愿意这么做。而分布式博弈迭代得到的纳什均衡解,每个产消者都是在给定他人策略下做自身最优决策,结果更符合个体理性,也因此更容易被各参与方接受。
3.2 ADMM框架下如何将全局问题分解为局部子问题
分布式优化的经典工具是交替方向乘子法,核心思路是“分而治之、迭代协调”。针对多产消者能量共享模型,通常的做法是引入辅助变量来解耦全局供需平衡约束。具体来说,将每个产消者与内部市场交互的净购电量记为变量,并引入辅助变量,使得所有产消者的净购电量之和为0的约束被拆分为每个产消者的一致性约束。
ADMM迭代分为三步:
- 第一步(x-更新):每个产消者在给定的内部电价和拉格朗日乘子下,独立求解自己的局部优化问题。这个子问题只包含自己的决策变量和约束,规模很小,用YALMIP加求解器或者自己写投影算法都能快速求解。
- 第二步(z-更新):协调者收集所有产消者的净购电量,更新辅助变量,使其满足总供需平衡。这一步通常有解析解,只是做一个平均或投影运算。
- 第三步(乘子更新):协调者更新拉格朗日乘子(也就是内部电价),将其反馈给各产消者,开始下一轮迭代。
这里要特别说明的是惩罚参数的选择。ADMM的收敛速度对惩罚参数非常敏感,我在实验中观察到,当内部电价与电网电价的量级差异较大时(比如内部电价在0.4-0.6之间,电网购电电价在0.8-1.5之间),惩罚参数取0.02-0.1之间比较稳定;如果惩罚参数取值过大,迭代前期会出现明显的振荡,需要加长每次迭代后乘子更新的步长来压制振荡;如果取值过小,收敛速度会非常慢,迭代次数可能超过500次才能达到精度要求。
3.3 纳什均衡与ADMM收敛解的关系:一个重要的等价性说明
经常有同行问:ADMM收敛到的是一个最优解,而博弈寻找的是纳什均衡,这两个概念能直接对应上吗?答案是“需要具体分析”。在单层博弈结构中,如果每个产消者的目标函数都是凸函数,策略空间是凸集,且内部市场的均衡条件恰好对应全局供需平衡约束的KKT条件,那么分布式ADMM迭代收敛到的稳定点,恰好就是博弈的纳什均衡。这也是这类模型在理论上比较优雅的地方:博弈论给出了均衡的概念框架,凸优化和ADMM给了求解工具,两边在法律上是一致的。但如果目标函数非凸(比如引入了二进制储能状态变量),那么ADMM只保证收敛到局部稳定点,不能严格保证它就是一个真正的纳什均衡,这一点必须在论文或报告中明确说明,否则容易被审稿人质疑。
4. MATLAB代码复现全流程:模块划分、核心函数与关键参数
4.1 代码整体架构与模块划分
我复现这套模型的MATLAB代码主要分为六个模块,对应的文件分工如下:
| 模块 | 文件名 | 功能说明 |
|---|---|---|
| 数据生成 | data_gen.m | 生成负荷曲线、光伏出力、储能参数、电价参数 |
| 参数设置 | params.m | 设置产消者数量、时段数、ADMM惩罚参数、迭代次数 |
| 局部求解 | local_solve.m | 求解单个产消者的局部优化子问题 |
| 协调更新 | coordinator_update.m | 更新供需平衡辅助变量和内部电价乘子 |
| 主循环 | main_admm.m | 调用上述函数,执行ADMM迭代主循环 |
| 结果分析 | plot_results.m | 绘制电价收敛、各产消者策略与成本分析图 |
模块划分的原则很简单:不要把局部优化逻辑和协调更新逻辑写在一个文件里,否则后续修改模型约束时会非常痛苦。我最初复现时偷懒把前两个模块合并了,后来想加入储能退化成本,改了一处目标函数导致所有逻辑都串在一起,花了整整一天排查。把功能拆开看着繁琐,但调试和扩展的效率反而最高。
4.2 局部子问题的求解:YALMIP与手写投影的取舍
局部子问题本质上是一个带二次效用函数和线性约束的小型优化问题,最简单的做法是直接用YALMIP建模后调用求解器求解。推荐使用gurobi或quadprog,纯线性约束加二次目标在几十个变量规模下,求解速度在毫秒级别。
关键代码结构如下:
% 单个产消者的局部优化子问题 function x = local_solve(p, lambda, rho, params) % p: 产消者参数结构体 % lambda: 当前内部电价(ADMM乘子) % 决策变量:购电量、售电量、储能充放电功率、储能SOC x_p2g = sdpvar(params.T, 1); % 向电网购电 x_g2p = sdpvar(params.T, 1); % 向电网售电 x_market = sdpvar(params.T, 1); % 与内部市场净购电 p_ch = sdpvar(params.T, 1); % 储能充电功率 p_dis = sdpvar(params.T, 1); % 储能放电功率 soc = sdpvar(params.T, 1); % 储能荷电状态 % 目标函数(二次效用 - 购电成本 + 售电收益 - 市场交易成本) objective = 0; for t = 1:params.T % 二次效用函数 utility = params.a(t) * p_load(t) - 0.5 * params.b(t) * p_load(t)^2; % 这里p_load是净用电量,包含基础负荷和储能净放电 purchase_cost = params.price_grid_buy(t) * x_p2g(t); sell_revenue = params.price_grid_sell(t) * x_g2p(t); market_cost = lambda * x_market(t) + 0.5 * rho * (x_market(t) - z(t))^2; end % 在这里注意:ADMM中引入的增广项不能漏掉,否则乘子迭代没有意义 % 约束条件 constraints = []; % 功率平衡约束 constraints = [constraints, p_load(t) == p_base(t) + p_dis(t) - p_ch(t)]; % 这里p_base是固定负荷,储能放电相当于增加可用电力,充电相当于增加内部消耗 % 市场净购电 + 电网净购电 = 实际用电缺口 constraints = [constraints, x_p2g(t) - x_g2p(t) + x_market(t) == p_load(t)]; % 储能约束 constraints = [constraints, soc(1) == params.soc_init]; constraints = [constraints, soc(t+1) == soc(t) + p_ch(t) * params.eta_ch - p_dis(t) / params.eta_dis]; constraints = [constraints, 0 <= soc(t) <= params.soc_max]; constraints = [constraints, 0 <= p_ch(t) <= params.p_ch_max]; constraints = [constraints, 0 <= p_dis(t) <= params.p_dis_max]; options = sdpsettings('verbose', 0, 'solver', 'quadprog'); optimize(constraints, objective, options); end这里需要特别提醒:不要把目标函数中的增广项遗忘。ADMM的局部更新中,目标函数不仅要包含自身运营成本,还要包含与辅助变量不一致的惩罚项,也就是里面的平方项,否则乘子迭代无法正常引导各产消者的策略走向全局平衡。
4.3 协调者更新:内部电价与辅助变量的迭代逻辑
协调者更新其实非常简洁,核心代码可以浓缩为:
% 协调者更新辅助变量 z 和乘子 lambda function [z, lambda] = coordinator_update(x_market_all, z, lambda, rho) N = length(x_market_all); % 产消者数量 % 更新辅助变量:使所有产消者的净购电量之和为0 mean_x = mean(x_market_all); % 这里z可以是每个产消者独立的辅助变量,也可以是一个公共变量 % 在总供需平衡约束下,最简单的方式是让z向平均值收缩 z = z + (x_market_all - mean_x); % 根据具体构建方式调整 % 更新乘子(内部电价) lambda = lambda + rho * (x_market_all - z); end需要说明的是,这个简写只是示意,具体公式取决于建模时的一致性约束写法。如果所有产消者共享同一个辅助变量z,那么z的更新就是求均值;如果每个产消者有自己的辅助变量,更新公式会稍微复杂。实际复现时,我建议先采用共享辅助变量的模式,因为代码逻辑最简单,收敛性也最容易观察。
4.4 迭代终止条件与收敛性判断
常用的迭代终止条件有两个:原始残差和对偶残差。原始残差反映的是供需平衡约束的违背程度,对偶残差反映的是决策变量的变化幅度。实际代码里可以简单地用相邻两次迭代的决策变量差值的无穷范数是否小于某个阈值来判断。
我的设置通常如下:
- 最大迭代次数:500
- 原始残差容忍度:1e-4
- 对偶残差容忍度:1e-4
- 每迭代10次记录一次中间结果,便于后续画收敛曲线
需要说明的是,这个容忍度设置与模型规模有关。如果是24个时段、10个产消者的小规模算例,1e-4的容忍度下通常迭代100-200次就能收敛;如果扩展到100个产消者,建议把容忍度放宽到1e-3,避免后期在很小的数值噪声上反复振荡。
5. 复现过程中遇到的坑:收敛振荡、参数敏感性与结果验证
5.1 惩罚参数引发的收敛振荡问题
我在第一次跑通代码后遇到的最典型问题,是内部电价和交易电量在迭代后期出现周期为2的振荡。排查后发现,原因是惩罚参数取值过小,导致乘子更新步长不足,各产消者之间的策略协调出现“过度反应——回调——再过度反应”的循环。解决办法有三个:一是增大惩罚参数,让每次迭代的一致性约束惩罚更强;二是对乘子更新引入一个松弛步长,也就是经典ADMM的Over-relaxation变体;三是将迭代终止条件中对偶残差的容忍度放宽,避免后期在不重要的数值噪声上反复迭代。
从工程角度,我建议优先尝试调整惩罚参数,每次按3到5倍幅度递增或递减,观察内部电价的收敛轨迹。这种方法虽然土,但比引入复杂的参数自适应机制要可靠得多,也更容易理解。
5.2 初始化策略对均衡结果的影响
分布式迭代算法对初始化很敏感,这是很多复现代码没有充分重视的点。如果初始内部电价设得过高,可能让所有产消者都倾向于卖电,导致第一轮迭代后供需严重失衡,需要很多次迭代才能纠正。相反,如果初始内部电价设得过低,则所有产消者都倾向于买电,同样不利于快速收敛。
一个比较稳妥的初始化方式是用电网的电价做加权平均,比如取购电价格和售电价格的中间值作为初始内部电价。在此基础上做对称化的初始策略,比如所有产消者初始规定的市场净购电量设为0,让市场在迭代初期保持平衡。我在多次实验中,这种初始化方式基本能保证100次迭代内达到1e-3精度的收敛。
5.3 如何验证得到的是纳什均衡而不是别的稳定点
这是论文审稿人最容易追问的问题:你怎么证明算法收敛到的就是纳什均衡?实用做法是收敛后逐一检查每个产消者——固定其他所有参与者的策略不变,单独改变该产消者的策略,验证其目标函数是否在收敛值处达到最优。如果任何单方面策略改变都不能提升其收益,说明确实达到了纳什均衡。
这个验证过程可以独立写一个函数:输入收敛后的策略组合,对每个产消者做一次局部搜索或重新求解局部优化问题,比较收益变化。我在多个算例中验证,只要模型是凸的、ADMM收敛精度足够,均衡验证都能顺利通过。但如果你在模型中加入了二阶锥约束或整数变量,就需要格外小心,因为非凸情况下ADMM可能收敛到伪均衡。
5.4 数值稳定性小技巧:归一化与缩放
一个容易被忽略但影响很大的细节是:不同变量的量级差异过大,会导致求解器数值稳定性变差。例如储能SOC通常在0到1之间,而用电负荷可能是几十千瓦,交易电费可能是几百元。如果不做任何归一化,Hessian矩阵的条件数会很大,ADMM的收敛速度会明显下降。
我的做法是把所有功率数据除以一个基准值(比如该时段最大负荷),所有价格数据除以一个基准电价,让所有变量在0到10的量级内。算完后在结果分析阶段再反归一化回来。这个技巧对任何大规模优化问题都适用,不是这个模型独有的经验。
6. 从基础模型到进阶扩展:鲁棒性、通信延迟与多时段耦合
6.1 鲁棒优化的扩展思路
共享能量模型在实际运行中面临的最大挑战是光伏出力和负荷的不确定性。为了处理这个问题,常见的扩展方向是将确定性模型替换为鲁棒优化模型,用不确定集合描述光伏出力的波动范围,使每个产消者的策略在光伏出力最差情况下仍然可行且收益有保证。实现方式是给局部子问题增加对偶变量,求解一个最小最大问题的等价形式。这个扩展在MATLAB里的实现并不复杂,但计算量会增加数倍,建议先跑通基础模型再逐步扩展。
6.2 通信时延与异步更新的模拟
当产消者数量增多、通信拓扑变得复杂时,所有参与者同步迭代的假设可能不再成立。一个简化处理方法是模拟非同步更新——每次迭代只随机选取部分产消者更新策略,其余产消者沿用上一轮的策略。这种异步ADMM变体在某些条件下仍能保证收敛,但收敛速度会明显变慢。我在一个包含50个产消者的算例中试过,同步迭代约120次收敛,异步更新(每次只更新30%的主体)需要约300次才收敛。这个实验可以作为一个研究点,写论文时能补充更丰富的数值讨论。
6.3 从单时段决策到多时段耦合:储能引入的时间耦合效应
前面提到的储能决策天然带有多时段耦合性——当前时段的充放电决策会影响未来时段的可用容量。这使得“分解到每个时段独立优化”的做法不可行。在ADMM框架中,每个产消者的局部子问题本身是一个跨时段的优化问题(我上面代码结构里是同一个优化问题包含所有时段变量,而不是逐时段求解),这就是经典的自洽处理方式。如果你看到某些论文把每个时段单独分解求解,那一定需要再引入跨时段储能耦合的额外协调机制。
7. 几个可直接“抄作业”的调试建议与个人体会
最后分享一些调试这套模型时的零散经验,都是踩过坑之后才总结出来的。
第一,永远在模型里先把“电网价格 > 内部交易均衡价 > 电网售电价格”的预期关系想清楚。如果复现结果出现内部电价高于电网购电价的情况,大概率是某条约束写错了,或者效用函数的参数设置得太极端——理性产消者此时根本不会参与内部交易,直接跟电网交易更划算。
第二,写代码时所有数组维度保持“时段数在前,产消者编号在后”的约定。这个看起来是小事,但分布式优化涉及多个参与者、多个时段时,维度混乱导致的bug极难排查,而且往往在结果图里看起来很合理,实际计算逻辑已经错了。我遇到过一整个算例结果画出来曲线合理但数值完全不对的情况,最后排查发现是某个矩阵转置弄错了。
第三,调试时先用两个产消者、两个时段的最小规模跑通。模型简化到一眼能算出预期结果的规模,然后逐步扩大。不要一上来就用10个产消者、24个时段的完整算例,否则出现了振荡你的注意力会被复杂的场景分散,根本定位不到问题根源。
第四,建议把每次迭代的内部电价保存成数组,画在图上观察演化过程。一个健康的收敛过程应该呈现“前期快速调整、中期单调趋稳、后期微小收敛”的形态。如果电价轨迹像心电图一样上下跳动,不用急着调参数,先检查供需平衡约束的符号方向,再检查辅助变量的更新公式——这类问题八成都出在符号或索引上,而不是参数上。
这套从博弈建模到ADMM求解,再到MATLAB代码复现的流程,整体难度其实不高,只要愿意花时间把每个模块的职责理清楚,大部分问题都能靠逐步调试解决。它最大的价值在于把两个原本分开的领域——微观层面的个体理性决策和宏观层面的市场供需平衡——用一套数学框架统一了起来,而且每个环节都可以用代码直接验证。后续如果要把这套模型推向实际应用,还可以在需求响应、公平分配机制、多时间尺度协调等方向继续延伸,但第一步还是要先把基础模型的代码写得干净、可复现、可验证。