ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多产消者非合作博弈能量共享的分布式优化与Matlab仿真

多产消者非合作博弈能量共享的分布式优化与Matlab仿真

这两年做电力系统优化方向的人,嘴里不挂个“产消者”都不好意思跟人打招呼。这类项目我前后接触过好几个版本,核心套路基本一致:屋顶光伏、储能、可调负荷都配齐了,几个相邻的产消者,既从电网买电,也互相之间买卖电,每个人都想让自己电费最少、收益最大。把这种“各打各的小算盘”用非合作博弈来描述,再用分布式优化去解,最后落到 Matlab 仿真,是一条很完整、也很有代表性的技术线。

这篇就围绕“基于分布式优化的多产消者非合作博弈能量共享”这个项目,把我自己的建模思路、数学写法、算法选型、Matlab 代码结构,以及调参踩过的坑,一次性讲清楚。适合正在做分布式能源交易、虚拟电厂、微电网能量管理相关课题的研究生,也适合刚入行想上手仿真的工程师。看完之后,你至少能搭出一个能跑、能出图、能写进报告里的仿真框架。

这类项目最容易被卡住的点不是理论,而是“从论文公式到 Matlab 代码”这一步。所以我后面会尽量按照实际写代码的逻辑来讲,而不是把公式堆在纸上就完事。

1. 项目定位:先搞清楚我们在解一个什么问题

1.1 多产消者能量共享不是简单的多买多卖

“产消者”这个词听起来新,但理解起来很容易:一个用户不再是单纯的消费者,自己屋顶有光伏、车库有储能,甚至有电动汽车,白天发电多、用电少,晚上发电少、用电多。单独看每一个产消者,都存在时间上的错配,于是就想办法在几个邻居之间做能量共享。

这个项目里的能量共享,并不是电网统一调度下的“大锅饭”。它更像一个小型电力交易市场:每个产消者都有自己的一本账,光伏发出来的电,可以自己用,可以卖给电网,也可以按协商价格卖给邻居。储能可以低价充电、高价放电,起到套利和调峰的作用。每个产消者都在权衡怎么分配自己的电量,能让收益最大、成本最小。

这里有个容易被忽略的点:能量共享和单纯“多卖电”不是一回事。共享过程中会出现多个交易对象,电网电价、邻居电价、光伏上网电价三条价格线同时存在。光伏余电卖给邻居,往往比卖给电网更划算;晚上从邻居买电,可能比从电网买还便宜。于是每个产消者之间形成了相互影响、相互制约的交易关系。

这种关系用大白话说,就是“每个人都是理性的,都想占便宜,但谁也不能强迫谁”。这正是非合作博弈最好的适用场景。

1.2 为什么非合作博弈是天然选择

如果所有产消者都属于同一个运营商,那问题就简单了:整体优化,统一调度,成本最小。但项目标题里明确写了“非合作博弈”,这暗示了一个前提——每个产消者是独立决策的主体,他们不会把自己完整的光伏数据和负荷数据交出去,也不会听从某个中央调度员的安排。

在非合作博弈框架里,每个产消者是一个参与者,决策变量是自己各时段的购售电量、储能充放电功率、与邻居的交易量。目标函数是自己的净收益,其他参与者的策略只能通过交易量和交易价格来间接影响。

每个产消者在做决策时,会把邻居当前的策略当作已知条件,然后求解自己的最优策略。所有人都在重复这个过程。如果最后谁都不愿意单方面改变策略,因为改了就会吃亏,那这个状态就是纳什均衡。

这个框架在工程上最直观的理解是:几个独立的“理性人”在讨价还价,最后达到一个大家都能接受的交易结果。它不是全局最优,但它是每个人都愿意接受的稳定结果。

1.3 分布式优化解决的核心矛盾

既然是多主体独立决策,那就不能再用中央式优化一把梭。中央式优化需要把所有人的数据集中起来,这在隐私和通信上都有问题。项目标题里的“分布式优化”,解决的核心矛盾就是:不集中数据,也能算出博弈结果。

分布式优化的思路是把耦合的全局问题拆成多个子问题,每个产消者只求解自己的子问题,然后通过少量边界信息与邻居交互。比如我只告诉你我准备跟你交易多少电,不告诉你我的光伏曲线和负荷曲线,更不告诉你我的储能成本和心理价位。这样既保护隐私,又减少了通信复杂度。

这个项目里最常用的分布式算法是 ADMM,也就是交替方向乘子法。它特别适合处理“两个主体之间交易量必须相等”这类耦合约束。数学上看着复杂,但工程实现其实很自然:每个产消者先算自己的最优交易量,然后和邻居核对,如果交易量对不上,就通过一个对偶变量调整价格,再重新算。如此反复,交易量逐渐对齐,价格也逐渐收敛。

所以整个项目的技术主线就可以概括为:非合作博弈负责建模,分布式优化负责求解,Matlab 负责实现和验证。

2. 数学模型:把每个“自利个体”写进优化目标

2.1 单个产消者的物理模型与决策变量

建模型之前,先把时间尺度和对象定下来。这个项目一般按一天 24 个时段建模,每个时段 1 小时。假设系统里有 N 个产消者,每个产消者 i 都配有光伏、负荷和储能。

先看单个产消者的物理模型。光伏出力 P_pv(i,t) 在光照时段是正的,可由典型曲线给出。负荷 L(i,t) 是必须满足的用电需求。储能电池有两个关键变量:充电功率 P_ch(i,t) 和放电功率 P_dis(i,t),一般用同一个变量的正负来表示也可以,但为了约束清晰,我习惯分开写。

储能还有荷电状态约束,也就是 SOC 在每个时段要保持在安全范围内,比如 0.1 到 0.9。SOC 的递推关系是:

SOC(i,t+1) = SOC(i,t) + eta_ch * P_ch(i,t) * Delta_t / Cap - P_dis(i,t) * Delta_t / (eta_dis * Cap)

这里的 eta_ch 和 eta_dis 是充放电效率,Cap 是电池容量,Delta_t 是时段长度。如果不写这个递推约束,储能就可以白嫖电量,仿真结果一点参考价值都没有。

每个产消者与电网的交易量记为 P_grid(i,t),正数表示从电网购电,负数表示向电网售电。与邻居 j 的交易量记为 x_ij(t),同样定义正数表示卖出,负数表示买入。

整个模型的决策变量就是:每时段的购售电功率、储能充放电功率、与每个邻居的交易量。变量数目大概是 N 个产消者乘以 24 个时段再乘以若干个通道,规模不大,但约束写起来很繁琐。

2.2 交易耦合约束是分布式优化的导火索

把单个产消者的模型拼到一起后,会出现一个非常关键的约束:产消者 i 卖给产消者 j 的电量,必须等于产消者 j 从产消者 i 买的电量。用数学语言写就是:

x_ij(t) + x_ji(t) = 0

这个约束看起来人畜无害,但它是所有“分布式”复杂性的来源。因为 x_ij(t) 是产消者 i 的决策变量,x_ji(t) 是产消者 j 的决策变量,两个变量分别属于两个不同的优化主体。没有任何一个主体能同时决定这两个变量。

在集中式优化里,这个约束只是矩阵里的一个等式行,求解器直接处理。但在分布式框架里,这个约束把两个独立的子问题“绑”在了一起。怎么解开这个绑定,就是 ADMM 要做的事。

除了交易耦合约束,每个产消者内部还有功率平衡约束:

L(i,t) + P_ch(i,t) = P_pv(i,t) + P_dis(i,t) + P_grid(i,t) + sum_j x_ij(t)

这个约束表示用电和充电,必须等于光伏、放电、电网购电和邻居交易的总和。它在每个产消者自己的子问题里是本地约束,不需要和邻居共享。

2.3 收益函数如何体现“博弈”

非合作博弈里,收益函数是每个产消者自己关心的事。常见写法是净支出最小化,或者收益最大化。我习惯把目标函数写成“净支出最小”,因为电网账单本身就是支出项,负支出就是收益,这样更贴近实际。

单个产消者的目标函数大致包含这么几块:

第一块是从电网购电的成本,等于电网购电电价乘购电量,只在购电方向发生。第二块是向电网售电的收入,光伏上网电价乘售电量,这个价格通常比购电价低。第三块是邻居交易的费用,交易价格乘交易量,交易量正负决定是收入还是支出。第四块是储能充放电的损耗成本,可以写成折旧系数乘充放电量的绝对值。

为了让问题保持凸性,很多项目会把交易价格当作可调参数,而不是决策变量。每一步迭代中,交易价格由对偶变量更新,产消者只响应这个价格。这样每个子问题都是一个带线性约束的凸二次规划,Matlab 的 quadprog 就能直接解。

一个典型的目标函数可以写成:

min sum_t [ cb(t) * max(P_grid(i,t),0) - cs(t) * min(P_grid(i,t),0) + lambda_ij(t) * x_ij(t) + C_bat * (P_ch(i,t) + P_dis(i,t)) ]

这个式子里的 max 和 min 写起来简单,但落到代码里不太友好。工程上通常把购电和售电拆成两个非负变量,分别设置上限,然后在线性目标里用不同系数。这样既避免非线性,又方便生成标准形式。

收益函数一旦能写成凸函数形式,后面用分布式算法求解就有了理论基础。如果目标函数里有非凸项,比如二进制变量、启动成本,那 ADMM 收敛性会变得很难看,容易出现震荡甚至不收敛。所以我的建议是:第一版先做线性或凸二次模型,把完整的非合作博弈流程跑通,再考虑加非凸细节。

3. 分布式求解算法与 Matlab 实现

3.1 算法选型:ADMM 为什么适合这个结构

多产消者能量共享的耦合约束,本质上是“成对变量相等”的约束。这种结构用 ADMM 处理非常顺手。

ADMM 的核心思想是:把耦合约束松弛到目标函数里,形成增广拉格朗日函数,然后交替更新原变量和对偶变量。每个原变量的更新只需要局部数据,对偶变量的更新只需要相邻主体的交换信息。

我见过一些人用梯度投影法或单纯对偶分解去做,但实际效果都不是很稳。因为交易量的可行域通常是有界闭合凸集,ADMM 在凸问题下有很好的收敛保证,而且对惩罚参数的敏感度相对可控。对非合作博弈求解来说,ADMM 的另一个好处是:它每一步迭代都有明确的经济解释。

什么经济解释?对偶变量可以看成交易价格。每次交易量对不上时,对偶变量就会调整,相当于市场里的价格信号发生了变化。价格升高,卖方愿意多卖;价格降低,买方愿意多买。最终交易量对齐,价格也不再变动,这个状态恰好对应博弈均衡。把这个解释写进论文或报告里,评委和导师都会觉得你很懂。

3.2 迭代格式与伪代码

整个分布式求解过程可以写成下面这个简洁的迭代循环。记产消者 i 与邻居 j 的交易量向量为 x_ij,对应的对偶变量(价格)为 lambda_ij。

每一步先固定对偶变量和邻居上一轮的交易量,产消者 i 求解自己的本地优化问题,得到新一轮 x_ij。然后所有产消者把新的交易量发送给邻居。接着更新每对交易的对偶变量:

lambda_ij = lambda_ij + rho * (x_ij + x_ji)

这里 rho 是 ADMM 的惩罚参数,控制交易量偏差对价格调整的力度。最后计算原始残差和对偶残差,如果残差小于允许误差,就认为收敛。

伪代码写成这样:

初始化 x_ij(0), lambda_ij(0), rho, max_iter, tol for k = 0, 1, 2, ...: for i = 1:N: 根据 lambda_ij(k), x_ji(k) 求解本地问题 得到 x_ij(k+1) end 交换所有 x_ij(k+1) 更新 lambda_ij(k+1) = lambda_ij(k) + rho*(x_ij(k+1)+x_ji(k+1)) 计算 r_pri = max(abs(x_ij(k+1)+x_ji(k+1))) 计算 r_dual = max(abs(rho*(x_ij(k+1)-x_ij(k)))) 如果 r_pri < tol 且 r_dual < tol,则收敛 end

这个伪代码虽然在工程上做了一些简化,但已经抓住了算法的骨架。具体到每个产消者的本地问题,还是要调用优化求解器去解。

3.3 Matlab 代码架构与关键函数

Matlab 代码的架构我建议按“主脚本 + 子函数”拆开,不要把所有内容堆到一个文件里。至少要有这几个文件:主仿真脚本、参数初始化脚本、单个产消者本地优化函数、ADMM 主循环函数、结果画图脚本。

我用过一个比较顺手的结构,主函数可以写成下面这个样子:

function [x, lambda, history] = admm_p2p(prosumer_data, neighbor_list, params) % prosumer_data: 结构体数组,包含每个产消者的PV、负荷、储能参数 % neighbor_list: 邻接关系,例如 {[2,3], [1,3], [1,2]} % params: rho, max_iter, tol, 电价参数等 N = length(prosumer_data); T = params.T; x = cell(N, N); % 交易量 lambda = cell(N, N); % 交易价格/对偶变量 for k = 1:params.max_iter for i = 1:N [x{i, :}, obj_i] = local_prosumer_update(i, prosumer_data(i), ... x(:, :, i), lambda(i, :), params); end % 更新对偶变量 % 计算残差 if max_residual < params.tol break; end end end

单个产消者的本地优化函数是核心。这个函数输入自己的光伏、负荷、储能参数、邻居上一轮交易量、当前交易价格,输出新一轮交易量。内部用 quadprog 或 linprog 求解。

如果目标函数是线性、约束是线性,可以直接用 linprog。如果加了储能折旧二次项,或者希望目标函数更平滑,就用 quadprog。下面是一个大概的写法:

function x_next = local_prosumer_update(pv, load, battery, price_grid, lambda, x_neighbor, rho) % 构造决策变量:电网购电、电网售电、储能充/放电、各邻居交易量 % 目标函数:购电成本 - 售电收益 + 交易费用 + 储能损耗 % 约束:功率平衡、储能SOC递推、交易量上下限 % 调用 quadprog 求解 % x_next 即本地产消者新一轮的交易量向量 end

代码里最容易写错的是变量索引。每个主体的决策变量顺序必须固定,比如 1 到 T 是购电,T+1 到 2T 是售电,2T+1 到 3T 是储能充电,等等。如果顺序不一致,quadprog 的对偶变量、约束矩阵、结果还原都会乱。我自己的习惯是,在函数开头写清变量索引注释,然后逐段构造 H、f、Aineq、bineq、Aeq、beq、lb、ub。

下一种常见的做法是用 YALMIP 或 CVX 建模,然后调用内置求解器。这类工具箱写起来更接近数学式子,适合快速验证模型。但如果你想把仿真嵌入到更大的循环里,反复调用 YALMIP 会导致额外开销,跑 24 时段的动态博弈会很慢。所以我给别人的建议是:小规模验证用 YALMIP,追求性能和调试体验就用 quadprog 手工建模。

3.4 关键参数怎么设

ADMM 里最敏感的参数是 rho。rho 太小,对偶变量更新缓慢,迭代次数很多;rho 太大,交易量会在最优解附近来回震荡,收敛路径很难看。

根据我的经验,rho 初值可以从 0.1 到 1 之间试。如果交易量量纲是千瓦,价格量纲是元/千瓦时,rho 取 0.5 左右通常是个合理起点。更高级的做法是动态调整:原始残差和对偶残差的比值过大时放大 rho,比值过小时缩小 rho。这个技巧在正规文档里不常见,但实测非常有效。

终止阈值 tol 一般取 1e-4 或 1e-5。交易量的残差单位是千瓦,取 1e-4 意味着交易量误差小于 0.1 瓦,已经足够精确。再小会拖慢收敛,而且对数据精度没有实际帮助。

最大迭代次数不要设置得太小。我曾经在 200 次迭代时看到残差已经降到 1e-3,但离真正的均衡还差一点;把上限放到 1000 次之后,多跑几十步就明显更稳。对整个仿真来说,几十个产消者、24 时段、几百次迭代,Matlab 完全扛得住。

4. 仿真算例:用三个产消者验证博弈均衡

4.1 算例数据准备

为了讲起来具体,我用一个三产消者的小系统来演示。三个产消者各自的“人设”要有差异,否则博弈结果不明显。

产消者 A 是“光伏大户”,屋顶光伏装机大,白天发的电远大于自己用,储能容量相对小。产消者 B 是“普通家庭”,光伏一般,负荷集中在晚上,储能中等。产消者 C 是“商业用户”,白天负荷高,没有太多光伏,但储能容量大,可以在低价时段充电、高价时段放电。

假设仿真周期是 24 小时,电网分时电价采用峰谷电价。光伏上网电价固定为比电网购电价更低的常数。这样设置的好处是,产消者之间交易有利可图,模型会产生明显的交易行为。

参数大致可以做成下面这个表格:

参数产消者A产消者B产消者C
光伏容量(kW)1003010
峰值负荷(kW)405080
储能容量(kWh)5060100
储能功率上限(kW)201530
初始SOC0.30.50.5

在实际代码里,这些参数会写成一个结构体数组。我习惯把光伏出力和负荷曲线分别放进两个 24 行的矩阵里,每个产消者一列,这样读取和画图都方便。

4.2 运行结果应该怎么看

第一次跑通代码后,不要急着看收益数字,先看三张图。

第一张是交易量收敛曲线。横轴是迭代次数,纵轴是所有交易对残差的最大值。正常情况是一条单调下降的曲线,可能有一些波动,但整体趋势向下。如果曲线完全水平或者来回跳动,说明参数或者模型有问题。

第二张是各产消者之间交易电价的变化曲线。交易电价从初始猜测值开始调整,最后会稳定在某一个值附近。这个稳定值应该在电网购电价和光伏上网电价之间,否则说明交易条件不合理。

第三张是各产消者 24 时段的功率平衡图。柱状图或堆叠面积图都行,把光伏出力、负荷、购电、售电、储能充放电画在同一张图上,可以直观看到能量共享在哪些时段发生。

比如在午间光伏高峰时段,产消者 A 的光伏出力超过自身负荷,它会把多余电量卖给 B 或 C。在晚间负荷高峰时段,C 可能用储能在白天存下的电来放电,或者从 A 买电。如果这些行为能对应上,说明模型逻辑是自洽的。

4.3 怎么判断结果达到纳什均衡

光看交易量收敛还不够,因为 ADMM 收敛只能说明分布式算法找到了一个可行解,不一定是博弈的纳什均衡。判断非合作博弈是否达到纳什均衡,最直接的方法是“单边偏离检验”。

具体做法是:让所有产消者按照最后迭代出的策略运行,固定住除产消者 A 以外所有人的策略。然后把 A 的策略放开,重新优化 A 的本地问题,看 A 的收益能不能比当前结果更好。

如果 A 重新优化后的收益比当前收益高出一个明显阈值,说明当前结果不是纳什均衡,A 有单方面改变策略的动机。如果每个产消者做一遍单边偏离检验,收益都没有明显提高,那就可以认为结果达到了纳什均衡。

这个检验做起来很简单,因为本地优化函数已经写好了,只需要把其他人的交易量固定住再调用一次。很多论文里不写这一步,但如果你在自己的报告里加上这个验证,会让结果非常有说服力。

还有一个更实用的判断方法:比较每个产消者在“参与共享”和“不参与共享”两种情形下的收益。参与共享后的收益如果不如独自运行,那任何一个理性产消者都会退出,项目就没有意义。正常情况下,共享后的收益应该比独自运行高,增加的收益来自时间错配和价格差异。

5. 常见问题与排查技巧

5.1 迭代震荡或不收敛

这是遇到最多的问题。如果 ADMM 迭代曲线像锯齿一样上下跳,首先要怀疑 rho 设置。rho 太大会导致交易量在可行域边界来回跳动。可以先把 rho 调小一个量级试试,比如从 1 改成 0.1。

如果 rho 减小后收敛了但速度很慢,那就需要自适应调整 rho。我写过最简单的自适应策略:每 20 步比较一次原始残差和对偶残差,如果原始残差大于 5 倍对偶残差,就把 rho 乘以 1.5;如果对偶残差大于 5 倍原始残差,就把 rho 除以 1.5。这样能在大部分情况下自动找到合适的 rho。

还有一种震荡原因是目标函数非凸。检查一下有没有引入二进制变量、分段线性成本、或者交易价格的乘积项。如果有,建议先简化成纯凸问题。

5.2 交易量对不上、能量不守恒

ADMM 迭代过程中,交易量对偶变量没有收敛前,x_ij 和 x_ji 并不严格互为相反数,这是正常的。如果最终收敛了还对不上,问题一般出在变量符号定义上。

我犯过的一个低级错误是:产消者 i 把卖电给 j 记为正,产消者 j 也把自己卖电给 i 记为正。这样两者的交易量符号就反了,更新对偶变量时无论怎么迭代都收敛不到 x_ij + x_ji = 0。排查方法很简单:在初始化时打印一对交易量的符号和量级,人工确认正负方向。

能量不守恒还有一个隐蔽来源:储能充放电效率处理不当。如果充电和放电分别用两个效率折算,但功率平衡公式里漏了效率项,就会出现“凭空多出电量”的情况。建议先用不储能、不含交易的最简模型验证功率平衡,再逐步加模块。

5.3 分布式解和集中式解对不齐

经常有人拿分布式博弈结果和集中式优化结果对比,发现两者不相等,就怀疑算法错了。这里要区分两个问题:如果集中式问题只是把所有人的目标函数相加,求系统总成本最小化,那么得到的是“社会福利最优解”,而不是非合作博弈均衡。

在非合作博弈里,每个产消者只关心自己的收益,不会主动牺牲自己的利益去成全全局最优。所以博弈均衡和集中式最优解之间的差额,反映的是“自私性带来的效率损失”,这本身是有意义的结果。不要强行让两个解相等,除非你额外定义了内部转移支付机制。

如果确实想让分布式算法复现集中式最优解,那必须把目标函数换成一致的社会总成本,并且让所有产消者按照同一个目标协调。那样做出来的就不是非合作博弈,而是分布式联合优化。

5.4 Matlab 实现中容易踩的坑

第一个坑是变量初始化。很多人习惯把交易量和价格向量初始化为全零,结果前几步没问题,但到 100 步后发现收敛变慢。全零初始化本身没问题,但 lambda 和 x 的量级差异会导致 ADMM 早期残差波动。更好用的是把 lambda 初始化为电网购电价和上网电价之间的某个值,比如峰谷电价平均值,这样更贴近最终结果,收敛更快。

第二个坑是 quadprog 的矩阵维度。目标函数里的二次项矩阵 H 必须是对称半正定,否则求解器报错或者给出无意义结果。如果 H 手工构造错了,可以用 H = (H+H')/2 强制对称,再检查最小特征值。对纯线性目标,直接用 linprog 就好,没必要硬套 quadprog。

第三个坑是结构体数组的更新。如果每个产消者内部还有多个对象,比如储能寿命状态、电动汽车状态,用结构体数组时很容易出现复制覆盖问题。建议把每次迭代产生的结果存成元胞数组或者独立的历史记录结构,避免上一轮数据被下一轮覆盖。

第四个坑是求解时间过长。分布式算法每次迭代都要调用 N 次优化求解器,如果每个本地问题变量很多,Matlab 的总耗时会长到不能接受。这时候可以试试减少变量数量,把可用决策变量压缩。比如购电和售电不拆成两个变量,而是用一个变量加上互补约束,有时反而更快。或者使用并行工具,因为每个产消者的本地求解在理论上是可以并行执行的。

最后分享一个我实际调试这个项目时用的小技巧:不要一上来就加储能、加多时段、加复杂电价。先做一个“单时段、无储能、两个产消者”的最简版本,跑通交易量耦合和 ADMM 循环;然后再加入 24 时段,再加入储能,最后加入完整的分时电价。每一次只在模型里增加一个变量,出现问题能立刻定位到是哪个模块的锅。

我按这个流程跑过几个版本的模型,最实用的一句话是:分布式优化最难的不是算法推导,而是把“每个主体只更新自己的变量、只交换必要信息”这个纪律贯彻到底。等你把三产消者的博弈结果跑出来,看到交易电价收敛、收益改善、单边偏离检验通过的那一刻,前面所有调参的烦躁都会变成值得。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进