十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多产消者非合作博弈能量共享:分布式优化建模与ADMM求解实践

多产消者非合作博弈能量共享:分布式优化建模与ADMM求解实践 前一阵有个师弟问我“基于分布式优化的多产消者非合作博弈能量共享”这类题目到底在研究什么Matlab代码又该从哪下手。我一听就明白他卡在哪了——这类工作横跨电力系统、博弈论和最优化三个方向从数学模型到可运行的代码中间藏着不少隐性门槛。本文我就以自己做过的一套仿真实验为例把这个题目的来龙去脉、建模思路和代码框架一次讲清楚顺便把调试时踩过的坑也一并交代。这套内容适合三类人一是刚接触产消者能量共享方向的研究生想快速理解题目逻辑二是已经在做分布式优化但搞不清非合作博弈和ADMM之间关系的开发者三是想把论文里的数学公式变成能出图的Matlab程序但不知道从哪里入手的工程师。读完之后你至少能回答三件事多产消者为什么要用非合作博弈建模内部电价协调的分布式求解思路怎么落地以及一套可以直接改参数运行的Matlab代码骨架长什么样。1. 问题背景与整体研究思路1.1 什么才算“多产消者”场景先说清楚研究对象。所谓产消者就是集生产与消费于一身的主体屋顶装了光伏的家庭、装了储能的小型工商业用户、拥有充电桩的车主都算典型产消者。以前电力系统是“发电厂集中发电、用户单向用电”但当一片区域里出现大量分布式光伏和储能之后潮流开始双向流动用户之间的能量交互在物理上已经可行。能量共享要做的事情就是让一个社区、一栋楼或一个园区内部的产消者优先互相匹配供需光伏大发时有余量的用户把电共享给缺电的邻居而不是全部反送电网夜间负荷高峰时大家从内部买电而不是全部从主网购电。这样既能降低对输配电网的依赖也能减少整体购电费用。在这个方向上典型的研究规模是4到10个产消者、24小时调度周期时间步长取1小时。这个规模在Matlab里求解毫无压力也是大多数论文验证算法有效性的起步配置。如果直接把规模提到50个节点、时间步长降到15分钟计算量会明显上升但算法框架不会变这也是这个方向很适合用Matlab做快速验证的原因。1.2 为什么用“非合作博弈”而不是集中式调度这个问题几乎每个刚接触该方向的人都会问。集中式调度的思路是假如所有产消者愿意把光伏出力、负荷曲线、储能状态全部上报给一个中心节点中心求解全系统总成本最小化问题然后下发每个产消者的最优充放电指令。理论上这能做到系统级最优总成本最低实现也简单。但现实中没有几个产消者愿意这么做。大家本来就是独立的主体设备参数属于私密信息负荷曲线牵扯生活习惯储能容量更不可能随便告诉别人。就算统一调度能省电费因为没有从属关系和强制力这套方案在执行层面根本推不下去。非合作博弈恰好承认每个参与者的“自利”属性。它不要求任何人把私有数据交给中心而是设定一个公平的交互机制让每个产消者在给定外部信号比如内部共享电价时独立优化自己的成本。所有产消者各自做决定最终在市场机制下收敛到一个大家都能接受的均衡点。这套逻辑和现实行为更贴合。有个生活类比很适合理解这件事几个房东共用一台变压器每家都想多开空调又少交电费。如果让一个物业经理统一调度所有人的用电计划理论上最省但大家根本不会听。现实是物业根据总负荷调节内部电价每家根据电价信号自己决定怎么用电最后形成一个“虽然不完美但大家都能接受”的状态。非合作博弈就是给这种“各自决策”提供一套严格的数学框架。1.3 从实际问题到数学模型要回答的三个问题把实际问题变成可计算的模型核心要回答三个问题。参与者是谁每个参与者的策略空间是什么收益或效用怎么量化。在这类能量共享研究中参与者就是N个产消者策略空间是每个产消者关于储能充放电、电网购售电和参与共享电量的决策效用函数则是调度周期内的总成本包括购电费用、售电收益和内部共享交易费用。这三个要素就构成了博弈模型的基础。完成这一步之后后面所有代码都围绕这三个要素展开。模型的分寸拿捏在“简单但不过度简化”既要保留产消者之间的耦合关系又不能把设备模型搞得比实际工程需求更复杂。储能模型用一阶SOC更新光伏和负荷用时间序列直接给定电网购售电用分时电价这样既能反映核心问题又不至于让代码被无关细节淹没。2. 核心数学模型搭建2.1 单个产消者的物理模型先定义基本变量。设产消者数量为N调度时段为T通常取24。每个产消者i在时段t的光伏出力用(P_{pv}(i,t))表示负荷用(P_{load}(i,t))表示与电网的交互分为购电(P_{grid_buy}(i,t))和售电(P_{grid_sell}(i,t))与内部市场的共享功率用(P_{share}(i,t))表示正值代表买入负值代表卖出。储能模型是这个问题的核心。设储能能量状态为(E(i,t))充放电功率分别为(P_{ch}(i,t))和(P_{dis}(i,t))那么SOC更新方程为[ E(i,t1)E(i,t)\eta_{ch}P_{ch}(i,t)\Delta t-\frac{P_{dis}(i,t)}{\eta_{dis}}\Delta t ]约束条件包括储能容量上下限、充放电功率上下限以及同一时刻不能同时充放电。光伏出力和负荷曲线在仿真中通常按时间序列直接读入不用建模光伏板的物理发电过程因为能量共享研究关心的是调度层策略不是光伏组件电气层特性。这里有个新手几乎必踩的坑时间步长(\Delta t)不是1的时候SOC更新方程里一定要记得乘上。很多人把时间单位写成小时采集间隔却是15分钟结果SOC变化量被放大了4倍最终的充放电策略完全不合理。我用kWh做能量单位功率用kW时间步长直接用小时数值参与运算这样量纲最直观。2.2 目标函数与成本构成每个产消者i的目标函数是在一个调度周期内最小化自己的总成本[ J_i\sum_{t1}^{T}\left[\pi_b(t)P_{grid_buy}(i,t)-\pi_s(t)P_{grid_sell}(i,t)\pi_{share}(t)P_{share}(i,t)\beta||P_{bat}(i,t)||^2\right] ]其中(\pi_b(t))是分时购电价(\pi_s(t))是售电价(\pi_{share}(t))是内部共享电价(\beta)是储能损耗惩罚系数。内部共享电价(\pi_{share}(t))是整个问题的灵魂变量。它不由外部给定而是在迭代求解过程中动态调整最终收敛到一个同时满足所有产消者最优性和市场出清条件的值。物理上它也应该落在购电价和售电价之间如果共享电价高于购电价产消者宁可向电网买电也不会参与内部共享如果低于售电价产消者宁可卖给电网也不愿内部卖出。这是后续判断仿真结果是否合理的第一道检验标准。储能损耗项(\beta||P_{bat}||^2)不是简单锦上添花它在数值计算中起着很关键的作用。目标函数里如果没有这个二次项子问题在某个方向上会存在平坦区域迭代时解容易来回漂移加了它之后每个本地问题变成严格凸问题最优解唯一迭代行为明显稳定。实际使用中(\beta)取0.01到0.05就能达到效果不必太大。2.3 耦合约束与博弈均衡的数学表述产消者之间能量共享的本质约束是功率守恒每个时段所有产消者的共享功率之和必须为零。[ \sum_{i1}^{N}P_{share}(i,t)0,\quad \forall t ]这组等式就是耦合约束它把各个独立的产消者捆绑在一起。没有它每个产消者都是单机优化整个问题退化为N个互不相干的最小化问题研究也就没有意义了。正是这组约束的存在使得我们需要分布式优化来协调各方。从博弈论角度我们把均衡条件形式化存在一个内部共享电价向量(\pi_{share})和一个策略组合使得每个产消者都在给定电价下实现了自身成本最小化同时市场出清约束成立。满足这两个条件就得到一个广义纳什均衡。这个表达方式的最大好处是把难以直接求解的博弈均衡问题转化成了一个相对直观的“搜索合适电价”的问题。每个产消者只需针对给定价格求解自己的凸优化问题然后由协调层根据总供需偏差更新价格循环往复直到出清。这就是分布式优化的用武之地。3. ADMM与价格协调分布式求解算法和Matlab实现3.1 为什么选ADMM思路而不是直接解方程组从数学上看均衡条件可以写成所有子问题KKT条件加市场出清约束的联立方程组理论上可以直接求解。但实际这样做的代价很高决策变量维度会随着产消者数量线性增长约束矩阵的规模迅速膨胀而且集中式求解把每个产消者的私有信息全部汇总到一处又回到了隐私问题的老路。ADMM的分解-协调思想正好对症。它把大问题拆成每个产消者自己解决的小问题再通过一个协调层把大家的解拉回一致。在这个能量共享模型里协调层只需要维护一个变量就是内部共享电价向量(\pi_{share})。迭代过程分为三步。第一步给定当前共享电价所有产消者并行求解自己的本地优化问题第二步汇总所有人的共享功率计算每个时段的总供需不平衡量第三步用不平衡量修正共享电价然后判断是否收敛。这个机制在经济学上对应“摸索出清价格”的过程在数学上等价于用带惩罚的对偶上升法求解耦合约束下的均衡问题也就是ADMM的一种工程化变体。3.2 价格更新与收敛性设计的几个关键参数价格更新公式是分布式求解的核心[ \pi_{share}^{(k1)}(t)\pi_{share}^{(k)}(t)\rho\Delta^{(k)}(t) ]其中(\Delta^{(k)}(t)\sum_{i1}^{N}P_{share}^{(k)}(i,t))是第k轮迭代时段t的总共享功率不平衡量(\rho)是步长。步长大小直接影响收敛行为。(\rho)过大价格更新时容易过度修正表现为价格来回震荡或者呈发散趋势(\rho)过小迭代推进太慢需要跑几百轮才能收敛。实践中我一般先取0.1到0.5之间的数值跑100轮画出收敛曲线再根据情况调整。更稳妥的做法是对不平衡量做归一化把(\Delta(t))除以系统内各产消者最大共享功率之和这样步长的取值范围与系统规模解耦换一组数据不用重新调参。收敛判据设置也要合理。主判据是每个时段的最大不平衡量小于一个阈值比如总负荷的1%以内辅助判据可以看相邻两轮共享电价的变化量如果已经很小说明迭代接近停滞点可以提前结束。共享电价还要设置上下限一般取购电价之上加一点余量、售电价之下减一点余量。上限设成购电价的1.2倍下限设成售电价的0.8倍可以防止数值异常时价格跑到不合理的区间。3.3 Matlab代码框架与核心函数实现我的代码组织方式是主脚本加三个子函数文件结构如下ADMM_energy_sharing_main.m generate_data.m prosumer_subproblem.m update_shared_price.m plot_results.m主脚本负责初始化参数、启动迭代循环、保存每轮结果。核心循环的Matlab代码如下%% 初始化 N 4; % 产消者数量 T 24; % 调度时段数 dt 1; % 时间步长单位小时 caseData generate_data(N, T); buy_price caseData.buy_price; % 1x24 购电价 sell_price caseData.sell_price; % 1x24 售电价 price_shared ones(1,T) * (buy_price(1) sell_price(1)) / 2; rho 0.3; % 价格更新步长 tol 0.01; % 收敛阈值 maxIter 200; residual zeros(maxIter, 1); history zeros(maxIter, T); %% ADMM 主循环 for k 1:maxIter P_share zeros(N, T); P_grid zeros(N, T); P_bat zeros(N, T); cost_i zeros(N, 1); % 各产消者独立求解本地子问题 for i 1:N [P_share(i,:), P_grid(i,:), P_bat(i,:), cost_i(i)] ... prosumer_subproblem(i, price_shared, caseData); end % 计算市场出清不平衡量 imbalance sum(P_share, 1); residual(k) max(abs(imbalance)); % 更新内部共享电价 price_shared_new price_shared rho * imbalance; price_shared min(max(price_shared_new, 0.8*min(sell_price)), 1.2*max(buy_price)); % 记录收敛过程 history(k, :) price_shared; % 收敛判定 if residual(k) tol fprintf(第%d轮收敛残差%.4f\n, k, residual(k)); break; end end子问题函数是整个代码的核心。产消者i给定当前共享电价求解自己的最小成本问题。我强烈建议直接用quadprog而不是fmincon原因很实际这个子问题的目标函数是二次型约束全是线性等式和不等式属于标准凸二次规划quadprog求解速度极快fmincon通用性强但对这类问题过度浪费在几十次ADMM迭代里会明显拖慢整体速度。子问题函数内部的变量定义和约束构造思路如下决策变量按顺序排列function [P_share, P_grid, P_bat, cost] ... prosumer_subproblem(i, price_shared, caseData) T length(price_shared); pv caseData.pv(i,:); load_ caseData.load(i,:); E0 caseData.E0(i); % 储能初始能量 Emax caseData.Emax(i); Emin caseData.Emin(i); PBmax caseData.PBmax(i); eta_ch caseData.eta_ch; eta_dis caseData.eta_dis; buy caseData.buy_price; sell caseData.sell_price; beta 0.02; % 决策变量顺序 % 1:T P_share % T1:2T P_grid_buy % 2T1:3T P_grid_sell % 3T1:4T P_bat_ch % 4T1:5T P_bat_dis % 5T1:6T-1 E(2) 到 E(T)储能中间状态变量 nvar 6*T - 1; % 目标函数二次项储能充放电的惩罚项 H zeros(nvar, nvar); for t 1:T H(3*Tt, 3*Tt) beta; H(4*Tt, 4*Tt) beta; end % 目标函数一次项 f zeros(nvar, 1); f(1:T) price_shared(:); % 共享电费 f(T1:2*T) buy(:); % 购电成本 f(2*T1:3*T) -sell(:); % 售电收益负值表示降低成本 % 等式约束矩阵 Aeq 和 beq % 1) 功率平衡每时段一行共 T 行 % 2) SOC 更新方程每时段一行共 T-1 行 % Aeq 的行数 2*T - 1 Aeq zeros(2*T-1, nvar); beq zeros(2*T-1, 1); % 功率平衡buy - sell dis - ch pv - load P_share 0 for t 1:T Aeq(t, t) 1; % P_share Aeq(t, Tt) 1; % P_grid_buy Aeq(t, 2*Tt) -1; % P_grid_sell Aeq(t, 3*Tt) -1; % P_bat_ch Aeq(t, 4*Tt) 1; % P_bat_dis beq(t) load_(t) - pv(t); end % SOC 更新E(t1) E(t) eta_ch*P_ch*dt - P_dis/eta_dis*dt for t 1:T-1 row T t; if t 1 Aeq(row, 5*T1) 1; % E(2) % 初始状态项移到右侧 Aeq(row, 3*Tt) -eta_ch * dt; Aeq(row, 4*Tt) dt / eta_dis; beq(row) E0; else Aeq(row, 5*Tt) 1; % E(t1) Aeq(row, 5*Tt-1) -1; % E(t) Aeq(row, 3*Tt) -eta_ch * dt; Aeq(row, 4*Tt) dt / eta_dis; beq(row) 0; end end % 边界约束 lb zeros(nvar, 1); ub zeros(nvar, 1); lb(2*T1:3*T) 0; ub(2*T1:3*T) PBmax; % P_grid_sell lb(T1:2*T) 0; ub(T1:2*T) PBmax; % P_grid_buy lb(3*T1:4*T) 0; ub(3*T1:4*T) PBmax; % P_bat_ch lb(4*T1:5*T) 0; ub(4*T1:5*T) PBmax; % P_bat_dis ub(5*T1:6*T-1) Emax; lb(5*T1:6*T-1) Emin; % 储能中间能量 % P_share 的正负由共享电价和购售电价共同决定物理可正可负 lb(1:T) -PBmax; ub(1:T) PBmax; % 求解 options optimoptions(quadprog, Display, off); x quadprog(H, f, [], [], Aeq, beq, lb, ub, [], options); if isempty(x) error(第%d个产消者的子问题无解请检查约束设置或初值, i); end P_share x(1:T); P_grid x(T1:2*T) - x(2*T1:3*T); P_bat x(3*T1:4*T) - x(4*T1:5*T); E_traj [E0, x(5*T1:6*T-1)]; cost x * H * x / 2 f * x; caseData.E_traj(i,:) E_traj; end注意一点如果把H矩阵的二次项两边都有1/2因子写成cost时要注意quadprog的目标是0.5*xHxf*x所以最后cost表达式的0.5因子不能漏。这个问题我调试时花了不少时间代码里注释要写清楚。3.4 为什么推荐先用小型算例验证逻辑我强烈建议拿到代码先跑一个N2的最小场景一个产消者光伏大发、自身负荷小另一个正好相反夜间负荷很大但没装光伏。这个场景下手工就能推算出大致结论共享电量会从有余电的一方流向缺电的一方共享电价会落在购电价和售电价之间两个产消者的总成本应当比各自单干更低。用这样的最小场景验证逻辑有两个好处。一方面规模小出问题容易定位是不收敛、价格越界还是SOC约束写错通过打印中间变量一眼就能看出来另一方面它给后续大规模仿真提供了一个可信基准——如果双产消者场景都解释不通扩展到10个产消者只会更难排查。4. 仿真结果分析与参数调节经验4.1 从输出曲线里应该看到什么仿真跑完之后至少要画四类图各产消者的储能SOC曲线、内部共享电价变化曲线、每位产消者的购售电与共享功率分布图、算法收敛残差曲线。正常的仿真结果应该呈现几个特征。光伏高峰时段有光伏余量的产消者共享功率为负把电卖给内部市场夜间负荷高峰时段缺电的产消者共享功率为正从内部买入。储能SOC曲线应当呈现削峰填谷的逻辑光伏充足时充电、夜间放电而不是毫无规律地随机充放。内部共享电价曲线应该始终位于购电价和售电价之间。如果某时段共享电价高于购电价产消者宁愿从电网买电市场机制在这个时段就失效了如果低于售电价产消者宁肯把电卖给电网也不会参与内部共享。这个不等式检查是结果合理性判断的第一关。成本对比也很重要。分别跑“不参与共享”和“参与共享”两个场景比较区域总购电成本。通常参与共享后区域整体购电成本会下降因为部分光伏余量在内部就被消纳了减少了从主网购电的量。但要注意博弈均衡并不保证每个产消者的成本都比单干时低在非合作框架下如果一个产消者因为自身约束太紧而无法套利可能出现成本不降反升的情况这是正常的博弈行为不是代码bug。真遇到这种情况需要检查是否因为价格初值或上下限设置把某类产消者的可行域逼得太紧。4.2 收敛性调试步长、上下界与初始点收敛性问题是这个项目里调试时间最长的一部分。最典型的现象是共享电价一直上涨但不收敛这时候第一反应是检查步长(\rho)是不是太大。我把(\rho)从0.3减到0.1价格震荡幅度立刻小了很多虽然迭代次数变多了一些但整体稳定了。另一个常见问题是共享电价在上下界之间来回弹。这种震荡往往不是步长问题而是价格上下限设置过窄。把上限从1.2倍购电价放宽到2倍给市场更大的调节空间震荡往往就消失了。还有残差下降速度极慢的情况特征是前50轮下降很快之后数百轮几乎不动。这时候将步长整体放大10倍再看如果残差曲线变陡了说明步长本来就太小。也可以配合在价格更新公式里加入动量项吸收ADMM里的惯性思想能显著加快收敛。共享电价的初始值也不容忽视。如果初始设置恰好等于购电价那么所有产消者在第一轮都没有卖出动力因为卖出价格和卖电网一样还要承担额外麻烦。我在代码里统一用购电价和售电价的平均值做初值实测下来各类场景的收敛表现都稳很多。5. 常见问题与实操避坑记录5.1 常见问题速查表我把调试过程中遇到的高频问题整理成了速查表供读者直接对照排查。现象可能原因排查与解决方法ADMM迭代不收敛价格来回震荡(\rho)过大或价格上下界过紧将(\rho)减半放宽(\pi_{share})上下限必要时做价格平滑每个产消者成本都高于预期储能末状态约束过严把严格等式改为(E(T1)\geq E0)的软约束给优化留自由空间收敛后共享功率不平衡量为零但电量对不上时间步长(\Delta t)忘记乘检查SOC更新方程统一使用kWh与kW让量纲自洽后再验证平衡方程共享电价卡在边界处上下限不合理或初始价格不当打印各时段购售电价重新设定(\pi_{share})限值某个产消者一直不参与共享初始共享电价低于其售电价或高于其购电价改用购售电价中位数初始化检查该产消者的可行域quadprog提示等式约束不兼容约束矩阵存在线性相关行检查Aeq是否包含冗余行确保SOC更新的各行线性独立fmincon求解子问题非常慢非凸求解器处理凸二次规划性能浪费改为quadprog或使用YALMIP建模但关掉调试输出5.2 储能SOC初末状态约束的坑储能约束是这类问题里最容易出“看似合理但结果怪异”的地方。很多版本把E(1)设成50%容量然后强制要求E(T1)E(1)意思是每天回到同样的起始状态。这个约束从长期运营角度没问题但会让储能完全失去跨日套利能力因为最后一天晚上必须存够电量回到初始值导致蓄意回购高价电成本反而升高。我常用的做法是末状态用不等式约束(E(T1)\geq E0)让优化自由决定末期电量只要不低于初始值即可。这样做既保证了系统的长期可持续性又给了储能足够的调度灵活性。若期刊或报告要求严格的每日循环再改成等式约束同时缩小末时段负荷的波动幅度避免出现不必要的回购。5.3 子问题用YALMIP还是quadprog如果你只是想快速验证模型YALMIP确实省心代码可读性也好。但在这个项目里ADMM主循环需要反复调用子问题几百次YALMIP每次建模都要重新生成符号变量和约束矩阵这个开销在多次迭代后会变得不可忽视。我的选择是原型验证阶段用YALMIP把模型逻辑写清楚确认无误后重写成quadprog形式。两种方式我都提供过代码逻辑一样但quadprog版本在同样的收敛条件下运行速度快了将近一倍。实验室跑大规模场景时这个差异直接决定了调参效率。5.4 数据生成阶段容易忽略的时间对齐问题光伏出力数据可能来自气象站分钟级记录负荷数据可能来自历史电表采样频率不一致时必须用interp1或resample统一到同一时间轴再进入模型。否则功率平衡方程在边界时段必然报错而且这种错误往往不会在第一天运行时暴露直到某一次SOC约束恰好紧绷时才跳出来极难排查。另一个实用建议是给数据生成函数设置固定随机种子。不同随机种子生成的光伏、负荷曲线差异很大有些参数组合下收敛快有些收敛慢如果不固定种子很难判断你调好的参数是不是只在某一组数据上有效。固定种子之后同一组数据重复跑结果完全可复现调参体验会好很多。最后再分享一个小技巧调试这类分布式优化代码我最大的经验是分阶段验证千万不要一上来就跑到N10的大规模场景。先把N设为1单独验证单个产消者的子问题求解是否正确检查SOC曲线是否合理目标函数是否满足功率平衡再把N设为2用一个“光伏富余负荷缺电”的算例验证共享电价的收敛逻辑都通过之后再把N逐渐增大观察收敛残差和总成本的变化曲线。这个逐级放大的调试流程能帮你把“数学模型推导错误”“凸优化建模错误”和“分布式算法参数问题”这三类错误区别开。如果不是一级一级排查当这几种错误混在一起时调试成本会成倍上升。这套流程在我后续做很多类似优化课题时也一直在用可以说是从踩坑里换来的最值得分享的经验。
返回列表