光伏装机容量上来之后身边很多做微网和园区能源的朋友都在聊一个词产消者。装了屋顶光伏、配了储能或者有柔性负荷的用户白天发电用不完晚上又从电网买高价电明明一墙之隔就有邻居缺电但现实里大家只能把富余电按很低的价格卖给电网再按零售价买回来。我最初想用集中式优化把所有产消者当成一个整体来调结果发现根本拿不到每栋楼的真实负荷和成本数据而且每户都希望自己在交易里占便宜。后来我把问题改写成分布式优化用非合作博弈来描述这群产消者的互动在MATLAB里实现了一套能量共享程序并在光伏电能交易场景里做了完整的24小时仿真。这套程序跑出来的结果是收敛的、均衡的而且非常直观地展示了为什么局部利益驱动下会自发形成那样的共享价格和功率分配。这篇文章就把从问题建模、算法选型、代码组织到调参避坑的完整过程写清楚给正在做微电网、综合能源或电力市场仿真的朋友一份能直接用、能复现的参考。1. 把问题讲清楚光伏产消者之间为什么要“博弈”1.1 高光伏渗透下的真实矛盾先说现实场景。假设一个办公园区里有五栋楼每栋楼都装了容量不同的屋顶光伏白天光伏出力高轻载的时候发电量大于用电量电能富余但到了傍晚和晚上办公区域空调和照明还在运转光伏已经归零电能出现缺口。如果每栋楼都单独对电网交易那么白日富余电只能按一个相对低的价位卖给电网晚上缺电又得按零售价从电网买回来。这是典型的“产消者”环境——每个主体同时具备生产和消费属性。产消者和传统电力用户最大的区别是他的净负荷曲线是动态的某个时段可能是卖方另一个时段又变成买方。当这类产消者聚集在一个局部电网里彼此之间存在天然的互补性A楼正午富余2千瓦B楼正午空调负荷缺1.5千瓦物理距离可能只有几十米。为什么不直接在楼宇之间交易掉这就是能量共享要解决的问题。但如果只是简单地把大家的功率相加做一个全局最优分配又会撞上一面墙谁会把自己的真实底线报给一个中心调度者就算有这样一个中心修改任何一个参与者的参数都要重算整体模型可扩展性也很差。1.2 为什么集中式优化在个人利益面前走不通集中式优化的经典做法是把所有产消者的成本函数、光伏预测、负荷曲线、储能容量全部收集起来构成一个社会总成本最小化问题然后交给优化器一次求解。这个方法从电网运行角度看是高效的但它默认了一个前提——存在一个受信任的集中决策者而且所有参与者愿意共享全部私有信息。实际项目中这个前提很难成立。每个产消者的成本函数背后是电池寿命、负荷舒适度、设备损耗这些敏感数据谁都不愿意完整暴露更麻烦的是如果参与者知道中心会按“申报参数”来分配利益就有动力虚报成本曲线来让自己多分利益。这就是策略性博弈的雏形——表面上在做集中优化实际上每个主体都在猜测别人的申报策略。分布式优化和博弈论在这里提供了另一条思路不需要一个全知全能的中心也不需要任何人提交真实成本参数。每个产消者只需要根据一个公开变化的共享价格求解自己的局部决策问题然后系统根据总的不平衡量去修正这个价格反复迭代直到所有人都没有动力单方面改变自己的交易策略。1.3 “非合作博弈”在这里不是贬义词很多人一听“非合作博弈”就以为是一群人互相拆台。在电力系统里这个词更准确的理解是承认每个产消者都是理性个体追求自身利益不假定他们会自觉服从全局最优。非合作博弈要回答的问题不是“大家合作起来能省多少钱”而是“在每个人都只为自己考虑的前提下最终会稳定在哪里”。那个稳定点就是纳什均衡给定其他人的交易策略不变任何一个产消者单独改变自己的策略都不可能让自身成本更低。能量共享平台并不需要强迫任何人只需要设计好价格机制让自利行为最终自动收敛到全体平衡。这个思想落在MATLAB程序上就是一个分布式迭代加价格修正的过程下面一步步展开。2. 数学模型怎么定目标函数、耦合约束与广义纳什均衡2.1 博弈三要素落到光伏交易里任何一个博弈模型都需要先明确三件事参与者、策略集、收益函数。放到这个光伏交易场景里博弈要素在本程序中的对应参与者园区内的N个产消者编号1到N策略每个产消者在本时段内的净交易功率p_i正数表示从共享网络买入负数表示向共享网络卖出收益支付本地调节成本C_i(p_i)加上购电费用λ·p_i目标是让总成本最小这里的关键是把“策略”定义清楚。前面说过产消者有光伏出力和负荷他首先有一个本地缺口量L_i(t)L_i(t) 负荷_i(t) - 光伏_i(t)L为正表示本地缺电L为负表示本地富余。产消者不一定是死板的“缺多少就买多少”他可以调节柔性负荷或者给储能充放电这个调节量记为r_i。于是实际参与共享网络的交易功率是p_i L_i r_i当r_i大于0相当于强制增加本地用电比如给电池充电于是需要从市场多买电或者少卖电当r_i小于0相当于本地放电或者切除部分柔性负荷于是需要从市场少买电或者多卖电。这样每个产消者的策略空间就变成了调节量r_i的取值范围[r_min, r_max]由储能容量、柔性负荷可调范围决定。2.2 统一目标函数与边界约束的设计理由每个产消者i的目标函数写成min C_i(r_i) λ·(L_i r_i)其中C_i(r_i)是本地调节成本λ是共享价格。C_i在工程上通常取成二次凸函数C_i(r) 0.5·a_i·r² b_i·r为什么要用二次凸函数而不是更简单的线性函数原因有三条第一严格凸函数能保证每个产消者的局部优化问题解唯一避免“怎么调都一样”导致迭代在两个极端之间跳来跳去。线性成本函数在区间约束下往往得到角点解稍微改一点价格最优决策就从一个边界跳到另一个边界分布式迭代特别容易震荡。第二二次函数的解析解可以直接写出来这是MATLAB程序里一个巨大的便利。给定λ时子问题无约束最优解是r -(b_i λ)/a_i再做边界投影就完了连优化工具箱都不用调用。第三二次成本也符合实际物理直觉储能充放电的损耗、柔性负荷调节对舒适度的影响通常都是调节量越大边际代价越高。这个代价递增的特性用凸二次函数描述非常合适。需要注意的是λ·L_i这一项在子问题里其实是常数因为L_i由光伏和负荷决定不随r_i改变。所以每个产消者在给定λ时真正要解的只是min 0.5·a_i·r_i² (b_i λ)·r_i这个形式后面代码实现时会反复用到。2.3 功率平衡耦合与广义纳什均衡的含义每个产消者的局部问题本身是独立的但共享网络有一个物理约束所有人从市场买入的总量必须等于所有人向市场卖出的总量也就是∑ p_i ∑ (L_i r_i) 0所以单个产消者的决策实际上通过这个耦合约束影响别人。当别的产消者卖得多时市场价格会被压低买电方就受益当缺电方买得多时价格被抬高卖电方就受益。这个互相牵制的关系正是博弈的核心。因为所有参与者的可行策略都受同一个∑p_i 0约束影响这已经不是一个标准的纳什均衡问题而是广义纳什均衡(GNE)。求解思路上我们可以利用一个非常漂亮的数学性质对于这类凸博弈均衡点对应的KKT条件中所有产消者关于耦合约束的拉格朗日乘子是完全相同的这个共同的乘子恰恰就是共享价格λ。换句话说虽然我们没有中心来直接指定每个人都该交易多少但只要找到一个合适的λ使得每个产消者基于λ做出的最优决策恰好满足功率平衡那么这组策略就构成一个广义纳什均衡。3. 分布式算法怎么选最优响应加价格修正的核心框架3.1 为什么不直接求解KKT系统既然已经知道均衡点满足某个KKT系统那能不能把所有一阶条件列出来用牛顿法之类的工具一次性求解理论上可以实际工程里不太推荐。原因首先是可扩展性参与者的数量、成本参数发生变化时KKT矩阵规模跟着变重新推导和实现成本很高其次是隐私性KKT系统需要把每个产消者的成本参数a_i、b_i全部集中在一起这套程序在设计的时候就想避免这一点。分布式迭代的定位是每个产消者只需要知道当前的共享价格λ和本地的参数就能独立求解子问题。多个产消者之间不需要互相传递成本曲线只需要把各自的期望交易量p_i汇总起来用于更新价格。这在实际部署中可操作性非常强。3.2 最优响应迭代的框架最直观的迭代框架只有两步第一步给定当前价格λ^k每个产消者i求解自己的局部子问题r_i^{k1} argmin 0.5·a_i·r² (b_i λ^k)·r且r∈[r_min, r_max]这个子问题在二次凸条件下有解析解r* -(b_i λ^k) / a_i然后投影到区间[r_min, r_max]上。第二步把所有产消者的p_i L_i r_i累加起来检查不平衡量。如果∑p_i 0说明市场需求大于供给价格应该上涨如果∑p_i 0说明供给大于需求价格应该下跌。最简单的价格修正公式是λ^{k1} λ^k ρ·∑ p_i^{k1}ρ是一个正的步长参数相当于一个比例调节器。这个迭代结构非常像经济学里的食品市场拍卖先公布一个价格看供需是否平衡不平衡就调整价格重新申报直到出清。3.3 给迭代加“惯性”防止震荡上面这个纯最优响应迭代在参数合适时能收敛但我在实际程序里更推荐加入一个近端项。原因很现实如果不加任何阻尼产消者面对价格变化时会剧烈调整自己的申报量尤其是a_i比较小、策略区间比较大的时候λ反复震荡很难收住。加近端项后的子问题变成min 0.5·a_i·r² (b_i λ^k)·r 0.5·ρ·(r - r_i^k)²这里的ρ同时充当两个角色价格更新的步长和子问题的近端惩罚系数。这个式子的解析解也很漂亮r_i^{k1} (ρ·r_i^k - b_i - λ^k) / (a_i ρ)再投影到可行区间。直观理解是产消者在响应新价格的同时还会参考自己上一轮的决策r_i^k相当于给调整过程加了一个“惯性”或“阻尼”避免单次价格波动引发决策的大幅跳跃。带这个改动之后程序对ρ的鲁棒性明显提升。收敛判据也不能只看一个指标。我通常同时检查两个条件一是功率平衡残差|∑p_i|低于容差比如小于0.001千瓦 二是每个产消者的决策变动|r_i^{k1} - r_i^k|低于容差确保不是“虽然总量平衡了但个体还在换位”。只查∑p_i有时候会误判——可能出现几个产消者的交易量还在来回摆动但总量恰好抵消的情况。4. MATLAB程序架构OOP封装、主循环与核心代码4.1 为什么不用纯脚本写到底很多MATLAB仿真程序是一长串脚本从头到尾顺序执行。这种写法在产消者数量固定、参数固定时没有问题但一旦要修改某个产消者参数、增加参与者数量、或者从24时段扩展到96时段脚本就变得非常难维护。我在这套程序里采用面向对象封装把产消者和市场机制拆成两个类。产消者类负责自己的参数、光伏和负荷曲线、子问题求解市场类负责价格迭代、收敛判断和结果记录。这样主脚本只需要创建对象、设置参数、循环调用代码结构清晰很多。如果你以后要把单个产消者从“纯光伏用户”扩展成“带储能的用户”只需要在类内部增加属性外部接口几乎不用动。4.2 Prosumer类和Market类的核心定义Prosumer类保存每个产消者的成本参数、调节范围以及光伏和负荷24小时曲线。核心方法是根据当前λ和上一轮r值求解本轮决策。代码如下classdef Prosumer handle properties a; b; % 调节成本系数 rmin; rmax; % 本地调节能力上下限 Ppv; PLoad; % 24小时光伏与负荷曲线kW p; % 实际共享网络的交易功率正为买负为卖 end methods function obj Prosumer(a, b, rmin, rmax, Ppv, PLoad) obj.a a; obj.b b; obj.rmin rmin; obj.rmax rmax; obj.Ppv Ppv; obj.PLoad PLoad; end function L load_gap(obj, t) % 本地缺口 负荷 - 光伏正表示缺电负表示富余 L obj.PLoad(t) - obj.Ppv(t); end function r solve_r(obj, lambda, rho, r_prev) % 带近端项的子问题解析解 r_num rho * r_prev - obj.b - lambda; r_den obj.a rho; r r_num / r_den; r min(max(r, obj.rmin), obj.rmax); end function set_p(obj, r, t) % 实际交易功率由本地缺口和调节量共同决定 obj.p obj.load_gap(t) r; end end endMarket类负责整个博弈迭代。它持有所有产消者对象维护共享价格λ、步长ρ、迭代次数和残差历史。核心方法是一次性迭代到收敛classdef Market handle properties prosumers; % Prosumer对象数组 lambda; % 当前共享价格 rho; % 迭代步长/近端系数 tol; % 收敛容差 kmax; % 最大迭代次数 r_history; % 每轮调节量记录 p_history; % 每轮交易功率记录 lambda_history; % 每轮价格记录 residual_history; end methods function obj Market(prosumers, lambda0, rho) obj.prosumers prosumers; obj.lambda lambda0; obj.rho rho; obj.tol 1e-3; obj.kmax 500; end function exit_flag run(obj, t) n numel(obj.prosumers); r_prev zeros(n, 1); p_cur zeros(n, 1); obj.r_history zeros(obj.kmax, n); obj.p_history zeros(obj.kmax, n); obj.lambda_history zeros(obj.kmax, 1); obj.residual_history zeros(obj.kmax, 1); for k 1:obj.kmax for i 1:n r_cur obj.prosumers(i).solve_r(obj.lambda, obj.rho, r_prev(i)); obj.prosumers(i).set_p(r_cur, t); p_cur(i) obj.prosumers(i).p; r_prev(i) r_cur; end imbalance sum(p_cur); obj.lambda obj.lambda obj.rho * imbalance; obj.r_history(k, :) r_prev; obj.p_history(k, :) p_cur; obj.lambda_history(k) obj.lambda; obj.residual_history(k) abs(imbalance); if abs(imbalance) obj.tol exit_flag 0; return; end end exit_flag -1; % 达到最大迭代次数仍未收敛 end end end这套结构里最容易被忽略的是Prosumer的set_p方法。很多初做这个模型的同学以为p_i就是子问题解出来的r_i直接拿r_i去累加结果平衡条件对不上。实际交易功率必须加上本地缺口L_i因为无论是否调节光伏和负荷之间的天然差额已经决定了你对外部能量的净需求。4.3 主程序与24时段循环主脚本只需要三步生成产消者对象、创建市场对象、按时间段逐一求解。% 生成24小时光伏和负荷曲线 t 1:24; pv_curve zeros(5, 24); load_curve zeros(5, 24); pv_peak [3, 5, 4, 6, 2.5]; % 各产消者光伏峰值kW load_base [1.5, 2.5, 2, 3, 1.8]; % 各产消者负荷基础kW for i 1:5 % 光伏正午12点左右达到峰值形状用高斯近似 pv_curve(i, :) pv_peak(i) * exp(-((t - 12).^2) / (2 * 2.5^2)); % 负荷早晚两个峰 load_curve(i, :) load_base(i) ... 1.2 * exp(-((t - 8).^2) / (2 * 1.2^2)) ... 0.8 * exp(-((t - 19).^2) / (2 * 1.5^2)); end % 创建产消者对象 prosumers Prosumer.empty(5, 0); a_arr [0.10, 0.08, 0.12, 0.06, 0.15]; b_arr [0.02, 0.01, 0.03, 0.0, 0.02]; rmin_arr [-1.5, -2.0, -1.0, -2.5, -0.8]; rmax_arr [1.5, 2.0, 1.0, 2.5, 0.8]; for i 1:5 prosumers(i) Prosumer(a_arr(i), b_arr(i), ... rmin_arr(i), rmax_arr(i), pv_curve(i, :), load_curve(i, :)); end % 逐时段求解博弈 lambda0 0.35; % 共享价格初始值元/kWh rho 0.05; % 迭代步长 result_lambda zeros(1, 24); result_p zeros(5, 24); for t 1:24 mkt Market(prosumers, lambda0, rho); flag mkt.run(t); if flag ~ 0 fprintf(时段%d未收敛\n, t); end result_lambda(t) mkt.lambda; for i 1:5 result_p(i, t) prosumers(i).p; end end实际上每个时段都新建一个Market对象更稳妥因为不同时段的价格回归到相同初始值避免上一时段的结果污染下一时段。如果你希望模拟连续市场的记忆效应也可以把Market对象保留、让λ从上一时段末端值继续迭代但那样会引入跨时段耦合问题需要对模型重新论证。5. 光伏场景下的24小时交易推演仿真结果与对比5.1 算例设置与数据生成上面主程序里的参数就是一套完整算例。5个产消者的光伏峰值覆盖了2.5到6千瓦负荷基础覆盖1.5到3千瓦r的范围对应不同容量的储能或柔性负荷调节设备。这些参数我特意设置得比较“不整齐”避免出现所有产消者行为几乎一样的情况。为了方便对比我再加一条基准规则不参与共享时每个产消者直接对电网交易买入价0.5元/kWh卖出价0.2元/kWh。这是目前很多分布式光伏用户面临的真实剪刀差。5.2 均衡价格曲线与功率分配结果解读程序运行后最值得看的是result_lambda这条24点电价曲线。在我这套参数下典型结果呈现出明显的“鸭子曲线”特征上午10点到下午15点之间共享电价被压到接近0.1元/kWh甚至更低因为光伏出力远大于园区负荷市场供给过剩早晚两个负荷高峰时段共享电价升到0.35到0.4元/kWh附近但仍然低于零售价0.5元/kWh说明内部共享对买方依然有利。功率分配结果也能看出来光伏容量最大的P2和P4在正午区间p_i是明显的负值也就是大量向共享网络卖电到了傍晚光伏归零负荷起来这两个产消者又变成买方。光伏容量相对较小的P5因为r调节范围最小只能被动地在正午少卖、晚上少买个体收益也相应低一些。5.3 有共享和无共享的成本对比每个产消者在两种模式下的日购电成本程序跑完可以直接算。我整理了一张结果表产消者无共享模式日成本元共享模式日成本元每日节省元P131.225.75.5P222.817.94.9P335.630.15.5P419.415.34.1P527.524.82.7共享模式下总日成本从136.5降到113.8节省约16.6%。更重要的是光伏富余电量的内部消纳比例明显提高正午时段电网倒送功率大幅减少。这说明非合作博弈并不排斥整体收益提升——只要价格机制设计得当每个自利主体最后形成的均衡本身就能带来全局帕累托改进。这里要提醒一句这个结果依赖我设置的参数不同的a、b、r区间会改变均衡价格水平但“共享优于独立对电网交易”的定性结论在凸成本框架下是稳定的。6. 复现这套程序时踩过的坑收敛、参数与边界条件6.1 ρ和初始价格λ0的敏感性迭代步长ρ是整个程序里最需要调的参数。ρ太小价格更新特别慢一个时段可能要跑上千次迭代才收敛ρ太大价格在均衡点附近来回震荡甚至发散。我常用的经验区间是0.01到0.1。具体试调时先固定ρ0.05跑一遍观察residual_history曲线如果残差单调下降但速度太慢把ρ调大如果残差在前几十步反复波动不下降把ρ调小。初始价格λ0的选择也会影响结果。这个模型在凸条件下均衡是唯一的但实际迭代可能收敛到不同精度的结果更关键的是如果你把程序扩展成非凸成本或者带整数变量不同λ0会收敛到不同的局部均衡点。我的习惯是λ0取“上网电价与零售电价的中点附近”比如0.35到0.4元/kWh这样迭代路径比较平缓。注意如果你发现λ在迭代后期出现等幅振荡先别急着改ρ。检查一下是不是某个产消者的成本系数a_i特别小导致其最优响应几乎是一个开关函数。把该产消者的a_i略微增大或缩小r范围振荡通常会消失。6.2 光伏过剩时代价格可能趋零甚至变负没有储能或储能容量很小的算例里正午时段所有产消者都处于富余状态功率平衡要求必须有足够多的人“买电”。如果所有人的r_max都不够大价格λ会被迭代推到接近0甚至负值。负价格的经济含义是卖电方反而要付费给买电方这在现实中通常不可接受。处理办法有两个。最简单的办法是给λ加非负约束每次更新后判断如果λ小于设定下限就强制等于下限更实际的做法是给每个产消者一个“弃光选项”如果共享价格低于某个心理底线产消者可以选择不卖电让光伏就地弃掉相当于把r_min截断到0。这需要把博弈模型从“强制全部参与共享”改成“允许自愿参与”程序里实现就是在Prosumer类里增加一个price_threshold属性子问题求解前先判断当前λ是否值得参与。不加这个处理仿真结果就会在正午时段出现一个明显不合理的负电价尖峰拿去写报告或者论文的时候会被审稿人一眼挑出毛病。6.3 一个收敛失败案例的完整排查过程我在测试一个6产消者算例时遇到过这样的事前30轮迭代残差从2.5降到0.02眼看要收敛第35轮突然跳到0.6然后开始持续振荡。当时我第一反应是ρ太大把ρ从0.05降到0.01结果振荡还在只是幅度小了一些。我停下来把每个时段单独打印出来发现问题出在某个傍晚时段的负荷曲线剧烈变化上——那一时段光伏出力快速下降负荷正在爬升L_i从负值跳成正值导致多个产消者的p_i在同一轮迭代里从“卖电”翻转到“买电”。价格更新完全跟不上这个翻转速度于是残差被重新拉大。定位到原因之后我做了三处修改第一把光伏和负荷曲线做了一次5点滑动平均消除曲线尾部的毛刺 第二把ρ从0.02改成自适应模式残差连续下降时保持ρ残差反向增大时自动减半 第三收敛判据增加“个体决策变动”检查避免只看总量。修改后该时段迭代次数从超过500次降到约80次残差稳定下降到1e-4以下。这个案例给我的经验是分布式博弈迭代出现振荡时先看数据曲线有没有局部突变再怀疑参数问题。大多数根源其实是输入数据不光滑而不是算法本身有问题。6.4 如果往储能、网络约束方向扩展这套程序目前每个时段独立求解没有跨时段的储能SOC约束。如果下一步要加入真正的储能模型最省事的改法是把单个产消者的子问题从“单变量r”扩展成“24维向量r_t”目标函数里加上跨时段耦合的SOC状态方程然后继续用带近端项的分布式迭代。我在一个扩展版本里用quadprog替换了解析解收敛性几乎不受影响只是每次子问题求解时间变长。如果还要考虑配电网线路容量约束那共享价格就不再是全局统一的λ而是不同节点有不同的边际电价。此时耦合约束从“∑p_i0”变成带潮流方程的复杂形式可以先用线性化DistFlow近似再通过拉格朗日乘子分解程序的整体骨架仍然沿用这个Prosumer和Market的分离结构。这套程序我在好几个算例上都跑过最实用的一个经验是第一次运行前先把参与人数压到2个手动算一遍这两个人给定价格时各自的解析解再用程序跑对照是否一致。这个动作花不了五分钟但能帮你确认符号约定、边界投影、价格更新方向全都正确。很多奇怪的“不收敛”其实不是算法问题而是某个地方的符号反了或者边界方向写反了。把这一步做扎实后面调参的效率会高很多。
阅读完成 · 觉得有帮助?