“基于分布式优化的多产消者非合作博弈能量共享研究”,光看这个题目确实有点劝退新手:分布式优化、多产消者、非合作博弈、能量共享,每一个词单拎出来都能写一本书,再叠加 Matlab 实现,很多人就卡在第一步了。其实拆开看,这就是一个特别生活化的场景——一个小区里住了好几户人家,有的装了屋顶光伏、有的装了家庭储能,白天光伏大发的时候,A 户电用不完,B 户正好缺电,两边与其各自跟大电网买卖,不如在小区内部形成一个“电量小市场”,自己定个价,互相买卖。谁都不愿意把自家用电数据全盘交给一个调度中心,也不想听谁统一指挥,那这个市场怎么出清?价格怎么定?这就是分布式优化和非合作博弈要解决的问题。这篇分享我会从数学模型讲起,把核心迭代算法讲明白,再把 Matlab 代码的框架和调试经验全部交代清楚,适合正在做微电网、综合能源、产消者交易方向的研究生,也适合想把优化算法真正落到代码里的工程师朋友。不整虚的,直接讲核心怎么做、代码怎么写、坑在哪里。
1. 先把课题掰开:产消者、博弈、分布式优化到底在说什么
1.1 产消者就是“会卖电的用户”
传统电力系统里,用户永远是用户,电网永远是电网,角色固定。但分布式光伏和户用储能普及之后,情况变了:一个普通家庭白天光伏大发时,发电量可能超过自家负荷,这时候它不但在用电,还在发电;到了晚上光伏归零,它又从“生产者”变回“消费者”。这种既能生产又能消费的主体,就是产消者(prosumer)。
这个身份转换不是文字游戏,它带来一个实打实的经济问题:多出来的电怎么办?最简单的方式是卖给电网,但上网电价和用户购电价之间通常有很明显的价差,有的地方甚至差一倍以上。卖给电网一毛五,从电网买回来六毛,里外里亏得慌。所以产消者自然希望找到附近同样装了光伏或储能、用电习惯互补的邻居,直接内部消化掉这部分电量。这就像周末在家做了很多菜吃不完,与其倒掉,不如低价卖给楼上懒得做饭的邻居,两边都比各自去食堂划算。
能量共享研究的价值就在这儿:让多出来的绿电在本地消纳,减少对大电网的依赖,同时让买卖双方都比单独跟电网打交道更省钱。不过,理想很丰满,真正操作起来有个核心障碍——那么多产消者,谁卖谁买、卖多少钱,怎么定?
1.2 为什么是非合作博弈,而不是合作博弈
面对多主体交易,一个直觉思路是:几个产消者联合起来,组成一个虚拟电厂,由一个运营商统一调度,大家按贡献分钱,这就是合作博弈的思路。合作博弈的数学模型确实漂亮,但它有两个现实前提很难满足:第一,所有成员要有统一的联盟目标,利润分配规则需要大家都认可;第二,各个成员的负荷、光伏出力、储能状态等核心数据必须向联盟公开,谁来调度就得看谁的底牌。
现实中的产消者可没那么好说话。邻居之间今天交换一下电量可以,明天你让他把一周的用电习惯、几点出门、装了多少储能全部报给一个“调度中枢”,他肯定不干,这涉及隐私,也涉及信任。更重要的是,每个产消者本质上都是理性的独立主体,他要的是自己成本最小、收益最大,而不是什么“联盟整体最优”。
所以非合作博弈才是更贴近现实的建模方式:每个产消者只优化自己的目标函数,通过市场机制(共享价格)来协调彼此的行为,谁也不用向谁交底。这里要理解一个关键概念,纳什均衡——在均衡状态下,给定其他人的交易策略,任何一个产消者单独改变自己的策略,都不可能让自己的成本更低或者收益更高。所有人都没有单方面偏离的动机,市场就稳定了。这种“各人自扫门前雪”的理性行为,恰好能在信息交换尽可能少的前提下让市场出清,这也是这个课题最吸引人的地方。
1.3 分布式优化:不交出隐私,也能算到均衡
如果所有产消者的数据都能汇总到一个中心节点,集中求解一个全局优化问题,理论上也能得到系统最优的共享方案。但集中式方案在真实场景里基本行不通,原因有三:
第一,隐私问题。负荷曲线、光伏出力、储能容量这些数据是产消者的核心隐私,集中式优化要求全部上报,很多人不愿意,数据合规上也有麻烦。第二,单点故障风险。整个系统依赖唯一的中心节点,一旦它宕机或通信拥堵,整个交易市场就瘫痪。第三,计算压力。产消者数量一多,集中式问题规模急剧膨胀,单个节点很难扛住。
分布式优化恰好绕开这三个问题。产消者不需要上报完整的负荷和光伏数据,只需要在每一轮迭代中告诉市场平台“我这个时刻想买多少或者卖多少电”,平台则只广播一个统一的共享价格给所有人。这个信息交换量极小、隐私暴露极低、任何单个节点故障也不影响整体系统。
打个比方:这就像菜市场里的价格谈判,摊主不用告诉菜贩子自己一天赚多少钱,只需要根据当前菜价决定进多少货;菜价高了就少买点,菜价低了就多囤点,来回几轮,市场自然就平衡了。分布式优化的核心就是这种“基于价格的协调机制”,用最少的信息交换,达到和集中式优化相差无几的均衡结果。
2. 数学模型怎么搭:先写目标函数,再谈市场均衡
2.1 每个产消者的局部优化问题
把问题写成数学形式之前,先约定符号。设系统里有 I 个产消者,每个产消者 i 有一套 24 小时的决策,为了简化,时段数 T=24,每个时段时长 \Delta t = 1 小时。每个产消者在每个时段 t 有已知的负荷 Load_i(t) 和光伏出力 PV_i(t),有容量为 Cap_i 的储能,储能充放电功率上限为 Pmax_i。
接下来是核心决策变量:产消者 i 在每个时段与内部共享网络交易的电量记为 x_i(t),正数表示向网络出售,负数表示从网络购买,单位是 kW;与外部电网交互也分成两部分,从电网购电 g_b(t) 和向电网卖电 g_s(t),两者都取非负值;储能充电功率 Pch(t) 和放电功率 Pdis(t) 同样是非负变量;另外引入储能荷电状态 SOC(t) 作为一个连续变量,方便写成线性约束。
功率平衡关系很直观,产消者 i 在时段 t 的净负荷 Load_i(t) - PV_i(t) 由三个来源平衡:储能出力、内部共享交易、电网交互,写成:
Load_i(t) - PV_i(t) + Pch_i(t) - Pdis_i(t) = g_b_i(t) - g_s_i(t) + x_i(t)
储能动态用 SOC 递推描述:
SOC_i(t+1) = SOC_i(t) + [\eta_{ch} Pch_i(t) - Pdis_i(t) / \eta_{dis}] \Delta t / Cap_i
其中 \eta_{ch} 和 \eta_{dis} 分别是充放电效率,这两个系数不一致会让 SOC 的累积产生细微不对称,实际仿真里要特别注意。SOC 要限制在合理区间,比如 0.1 到 0.9,不能过充过放,否则会加速电池老化。
目标函数是每个产消者最小化自己一天的总成本。这里要把钱算明白:从电网买电要花钱,单价为 \pi_{buy},即成本项 \pi_{buy} g_b(t);向电网卖电有收入,单价为 \pi_{sell},即收益项 -\pi_{sell} g_s(t)(注意负号是把收益折算进成本);与内部共享网络的交易按共享价格 \lambda(t) 结算,若 x_i(t)>0 卖出则有收入,若 x_i(t)<0 买入则有支出,所以这一项统一写成 -\lambda(t)x_i(t);储能充放电本身有能量损耗和老化成本,用一个小的惩罚系数 \eta_{loss} 乘以 Pch+Pdis 来近似。
总的局部目标就是:
min \sum_{t=1}^{T} [\pi_{buy} g_b(t) - \pi_{sell} g_s(t) - \lambda(t)x_i(t) + \eta_{loss}(Pch(t)+Pdis(t))]
这个目标函数是线性的,约束条件里除了 SOC 递推是等式约束外,还有变量上下界约束,以及一个避免储能同时充放电的不等式约束 Pch(t)+Pdis(t) \leq Pmax。整问题是一个线性规划(LP),Matlab 的 quadprog 或 linprog 都能直接求解。
2.2 共享价格与广义纳什均衡(GNEP)
上面的局部优化问题有一个隐藏的关键:内部共享价格 \lambda(t) 不是常数,它由所有产消者共同决定。当给定 \lambda(t) 时,每个产消者解出自己的 x_i(t),但把所有产消者的 x_i(t) 加起来,往往不等于零。而内部共享市场必须满足电量守恒:
\sum_{i=1}^{I} x_i(t) = 0
也就是说,有人卖电就必须有人买电,买卖量完全抵消。这个条件不能作为某个产消者的局部约束,因为没有任何一个主体有义务单方面平衡市场。它只能作为整个系统的耦合约束,通过调整价格来实现。
这就让问题从普通的纳什均衡问题(NEP)变成了广义纳什均衡问题(GNEP),难点在于:所有产消者的策略通过同一个共享价格\lambda 耦合在一起,任何一个主体改变策略都会影响价格,进而影响所有其他人的最优决策。严格来说,精确求解 GNEP 要把所有产消者的最优性条件(KKT 条件)联合起来,加上市场出清约束,形成一个包含互补约束的复杂系统,求解难度高很多。
幸运的是,工程上不需要直接解那个复杂的 KKT 系统,而是可以通过迭代逼近:价格更新 + 最优响应。这背后是经济学里的古典“拍卖者”(tatonnement)思想,平台先喊一个价格,所有交易者根据价格报出自己的买卖量,如果总需求大于总供给就涨价,反之就降价,多轮下来,市场逐步收敛到均衡价格。这个机制天然适合分布式实现,也是我们接下来 Matlab 代码的核心结构。
2.3 分布式迭代算法框架:价格更新加最优响应
综合上面分析,我给出一个能直接落地的分布式求解框架。平台端的任务是维护一套共享价格 \lambda(t),t=1,...,T,产消者端各自求解局部优化问题。完整步骤如下:
- 初始化共享价格 \lambda^{(0)}(t),一般取区间 [\pi_{sell}, \pi_{buy}] 内的值,实践经验是直接取 \pi_{buy},收敛更快。
- 平台把当前价格 \lambda^{(k)} 广播给所有产消者。
- 每个产消者独立并行求解自己的局部优化问题,得到最优交易量 x_i^{(k)}(t) 和最优储能策略。
- 产消者把 x_i^{(k)}(t) 上报给平台,平台计算总不平衡量 \Delta^{(k)}(t) = \sum_i x_i^{(k)}(t)。
- 平台更新价格:\lambda^{(k+1)}(t) = \lambda^{(k)}(t) - \alpha \Delta^{(k)}(t),并把价格投影回 [\pi_{sell}, \pi_{buy}] 区间。
- 检查收敛条件,比如 \max_t |\Delta^{(k)}(t)| < \varepsilon 或者价格变化小于阈值;若不满足,回到第 2 步。
步骤 5 里的负号很好理解:如果某个时段总出售量大于总购买量,\Delta(t) > 0,说明市场供大于求,价格就应该下降;反过来 \Delta(t) < 0 时供不应求,价格上升。这和一个真实市场里的供需关系完全一致。
步长 \alpha 的选取很关键,它决定迭代是否收敛、收敛多快。理论上当目标函数凸、可行域有界、价格步长足够小时,这个迭代会收敛到 GNEP 的均衡解。实际仿真中 \alpha 太大容易振荡,太小又过于缓慢,后面我会给出具体的调参经验。
3. Matlab 落地:把算法写成能跑的代码
3.1 运行环境与数据生成
先说环境:这个项目只需要 Matlab 基础版本加 Optimization Toolbox,因为要用到 quadprog 或者 linprog。版本无所谓新旧,2018b 之后的都行,2023b、2024a 甚至更新的版本都可以,不需要额外装 Yalmip 或 CVX,虽然装了也能做,但没必要为一个线性规划引入额外的依赖。
数据生成是整个仿真最容易被忽视的环节。很多人一上来就写好优化代码,结果数据乱造,导致结果没法解释。我的做法是分三步:先造负荷曲线,再造光伏出力曲线,最后设置储能参数。
负荷曲线要体现“早高峰、晚高峰、夜间低谷”的典型特征,不能是纯随机数。可以用一条典型家庭日负荷模板乘以不同系数来区分不同用户,这样既保留了真实性,又让各产消者之间有些差异。我常用的模板大致是这样的:
T = 24; base_load = [0.4 0.35 0.30 0.28 0.30 0.50 0.90 1.30 1.20 1.00 ... 0.90 1.10 1.20 1.15 1.00 0.95 1.10 1.40 1.60 1.30 ... 1.00 0.80 0.70 0.50]; % 典型家庭负荷形状,单位 kW % 每个产消者基础负荷略有差异 Load = zeros(I, T); for i = 1:I Load(i, :) = (0.8 + 0.4 * rand(1,1)) * base_load + 0.05 * randn(1, T); Load(i, :) = max(Load(i, :), 0.05); % 避免出现负负荷 end光伏出力曲线本质上是“日出到日落”的钟形曲线,中午最大、早晚为零。可以用一个归一化出力系数乘上每个产消者的光伏装机容量:
pv_curve = [0 0 0 0 0 0 0.1 0.35 0.65 0.90 1.10 1.20 1.20 ... 1.05 0.85 0.60 0.30 0.10 0 0 0 0 0 0]; % 归一化光伏出力 PV_capacity = [5 3 0]; % 三个产消者的光伏容量,kW PV = PV_capacity' * pv_curve;这里要让不同产消者的光伏容量不同,甚至有一个没有光伏的纯消费者,才能体现出能量共享的价值。储能参数方面,典型户用储能容量 5 kWh,最大充电/放电功率 2 kW,充放电效率 0.95,SOC 初始值设 0.5,范围 0.1 到 0.9。电网购电价 \pi_{buy} 取 0.8 元/kWh,上网电价 \pi_{sell} 取 0.3 元/kWh。
3.2 单主体优化子函数:把约束组进 quadprog
这是整个代码里最核心的部分。每个产消者面对的是一个带线性目标和线性约束的二次规划退化为线性规划的问题,quadprog 完全能处理。关键是变量的排列方式,排不好会让矩阵组装变成噩梦。
我建议把一个产消者的决策变量排成一个长向量 z,顺序是:内部交易量 x(24 个)、购电量 g_b(24 个)、售电量 g_s(24 个)、充电功率 Pch(24 个)、放电功率 Pdis(24 个)、末端 SOC(24 个)。总变量数 6T=144。既然 SOC 也作为变量出现,就能把 SOC 递推式写成线性等式约束,而不是在代码里做非线性递推。
目标函数 f 向量按同样的顺序组装:
function [x_opt, fval] = solve_prosumer(i, lambda, Load, PV, params) T = length(lambda); nvar = 6 * T; % 目标函数系数 f,对应变量排列顺序 f = zeros(nvar, 1); f(1:T) = -lambda; % 内部交易项:-\lambda * x f(T+1:2*T) = params.pi_buy; % 购电成本 f(2*T+1:3*T) = -params.pi_sell; % 售电收益(作为负成本) f(3*T+1:4*T) = params.eta_loss; % 充电损耗惩罚 f(4*T+1:5*T) = params.eta_loss; % 放电损耗惩罚 % SOC 部分不产生目标系数,f(5*T+1:6*T) 保持为 0 % 等式约束:功率平衡 + SOC 递推 % Aeq * z = beq n_eq = 2 * T; % 功率平衡 T 个,SOC 递推 T 个 Aeq = sparse(n_eq, nvar); beq = zeros(n_eq, 1); % 功率平衡约束:Load - PV + Pch - Pdis = gb - gs + x % 写成 -x + gb - gs - Pch + Pdis = -(Load - PV) for t = 1:T Aeq(t, t) = -1; % x(t) Aeq(t, T+t) = 1; % gb(t) Aeq(t, 2*T+t) = -1; % gs(t) Aeq(t, 3*T+t) = -1; % Pch(t) Aeq(t, 4*T+t) = 1; % Pdis(t) beq(t) = -(Load(i,t) - PV(i,t)); end % SOC 递推约束:SOC(t+1) = SOC(t) + (eta_ch*Pch(t) - Pdis(t)/eta_dis)*dt/Cap for t = 1:T Aeq(T+t, 5*T+t) = -1; % -SOC(t+1)? 这里按第t时段末SOC_t if t > 1 Aeq(T+t, 5*T+t-1) = 1; % +SOC(t) end Aeq(T+t, 3*T+t) = params.eta_ch * params.dt / params.Cap; % +eta*Pch Aeq(T+t, 4*T+t) = -1 / params.eta_dis * params.dt / params.Cap; % -Pdis/eta beq(T+t) = 0; end % 如果把 SOC 变量定义为时段末 SOC,需要在第一时段加入 SOC0 % 这里简单处理:t=1 时段时,SOC(t) 用初始 SOC0 替代,写进 beq % 不等式约束:Pch + Pdis <= Pmax(防止同时充放电) A = sparse(T, nvar); b = params.Pmax * ones(T,1); for t = 1:T A(t, 3*T+t) = 1; % Pch(t) A(t, 4*T+t) = 1; % Pdis(t) end % 变量上下界 lb = zeros(nvar, 1); ub = inf * ones(nvar, 1); lb(1:T) = -params.xmax; % x 可以买可以卖,带负下界 ub(1:T) = params.xmax; % 内部交易量上限 ub(T+1:2*T) = params.gmax; % 购电上限 ub(2*T+1:3*T) = params.gmax; % 售电上限 ub(3*T+1:4*T) = params.Pmax; % 充电功率上限 ub(4*T+1:5*T) = params.Pmax; % 放电功率上限 lb(5*T+1:6*T) = params.SOC_min; % SOC 下限 ub(5*T+1:6*T) = params.SOC_max; % SOC 上限 % 调用 quadprog,H 设为空矩阵表示线性目标 options = optimoptions('quadprog', 'Display', 'off'); H = sparse(nvar, nvar); [z_opt, fval] = quadprog(H, f, A, b, Aeq, beq, lb, ub, [], options); x_opt = z_opt(1:T); end上面这段是核心示意,有几点要重点说明。第一,SOC 递推约束我用了一个简化的写法,真正实现时要注意初始 SOC 的处理,最稳妥的办法是把 SOC(1) 对应时段 t=1 的递推式写成 SOC(1) = SOC0 + 效率相关项,其中 SOC0 是给定初值。第二,功率平衡约束我统一把变量移到左边,常数项移到右边,符号别弄反了,这是组装矩阵时最容易出的错。第三,quadprog 在目标为线性的时候依然可用,H 传一个全零稀疏矩阵就行。
我自己调试时踩过的一个坑就是 Aeq 组装符号问题:功率平衡约束写反正负号,结果就是储能疯狂充放电,各种违反物理直觉。后来想了一个笨办法,把一个产消者单独拎出来,固定 \lambda 为一个常数,然后和手工计算的结果对比,很快就定位了问题。
3.3 主循环实现:市场平台与产消者的信息交互
有了单主体优化函数,主程序就清爽很多。先初始化共享价格,然后进入迭代循环,每一轮广播价格、并行求解各产消者问题、汇总交易量、更新价格、检查收敛。
clear; clc; close all; rng(2025); %% 参数设置 I = 3; % 产消者数量 T = 24; % 时段数 dt = 1; % 时间步长 1 小时 pi_buy = 0.8; % 电网购电价(元/kWh) pi_sell = 0.3; % 电网售电价(元/kWh) alpha = 0.05; % 价格更新步长 maxIter = 300; tol = 1e-3; params = struct('pi_buy', pi_buy, 'pi_sell', pi_sell, 'dt', dt, ... 'Pmax', 2, 'Cap', 5, 'eta_ch', 0.95, 'eta_dis', 0.95, ... 'eta_loss', 0.01, 'xmax', 5, 'gmax', 5, ... 'SOC_min', 0.1, 'SOC_max', 0.9, 'SOC0', 0.5); %% 生成数据(代码同上,省略) % Load, PV 尺寸均为 I x T %% 初始化价格 lambda = pi_buy * ones(T,1); %% 分布式迭代 X = zeros(T, I); % 每列是一个产消者各时段的内部交易量 hist_imbalance = zeros(maxIter, 1); for k = 1:maxIter % 各产消者以当前价格独立决策 for i = 1:I [x_i, ~] = solve_prosumer(i, lambda, Load, PV, params); X(:, i) = x_i; end imbalance = sum(X, 2); % 总不平衡量 hist_imbalance(k) = norm(imbalance, inf); % 价格更新:供大于求则降价,供不应求则涨价 lambda = lambda - alpha * imbalance; lambda = min(max(lambda, pi_sell), pi_buy); % 投影到可行区间 % 收敛判断 if hist_imbalance(k) < tol fprintf('第 %d 轮收敛,最大不平衡量 %.6f\n', k, hist_imbalance(k)); break; end end这个结构已经是一个完整可运行的分布式框架。如果要扩大产消者数量,可以把内层 for 循环改成 parfor,因为每个产消者在给定价格下是独立求解的,天然可并行,不需要额外同步。不过在 parfor 下要注意 Lambda 是共享只读变量,X 各列独立写入,是符合并行条件的。
3.4 从结果里读出博弈均衡的物理含义
迭代结束后,手上有一堆输出:价格 \lambda、各产消者的交易量 X、目标函数值、还有每个产消者的 SOC 轨迹。这些数据不能只放在工作区里,要及时可视化和保存。
我个人建议画三张图。第一张是共享价格的收敛轨迹,可以画 4 到 6 个代表性时段的 \lambda 随迭代次数的变化,观察它是收敛还是振荡。第二张是 24 小时交易量热力图,横轴是时间,纵轴是产消者序号,颜色表示买电(蓝)还是卖电(红),一看就知道谁在什么时候出力。第三张是各产消者储能 SOC 曲线,用来检查储能策略是否合理,比如有没有出现过充过放、有没有明显的同充同放现象。
还有一个特别重要的对比基准:不参与共享时各产消者各自跟电网交易的成本,参与共享后的成本,两者相减就是能量共享带来的经济收益。这个对比值往往比一堆收敛曲线更能说明模型的实用价值,写论文或者做汇报的时候也是最有说服力的指标。
4. 仿真结果怎么看、参数怎么调
4.1 一个典型均衡结果长什么样
以三个产消者为例:A 有 5 kW 光伏,B 有 3 kW 光伏,C 没有光伏只有储能。这个配置很经典,因为 A 是“电力富余户”,C 是“纯用电户”,B 介于中间。
运行收敛后,典型的均衡结果是:中午 10 点到 15 点,A 的光伏出力大于用电需求,它会以内部共享价格把多余电量出售给 C,共享价格会高于电网收购价,所以 A 比直接卖给电网赚得多;而 C 从内部买电的价格会低于电网购电价,所以 C 也比直接买电网电省钱。到了傍晚,A 的光伏归零,如果它有储能,它会在中午低价时段充一部分电留到晚上用;如果没有储能,它会变成买家,和 C 一起争夺 B 可能的余电。
这里有一个值得标注的现象:收敛后的共享价格 \lambda(t) 会落在电网售电价和购电价之间,而不是等于某一个极值。原因很直观:如果 \lambda 高于 \pi_{buy}(购电价),谁也不傻,都去从电网买电,内部市场没人买;如果 \lambda 低于 \pi_{sell}(上网电价),谁也不想在内部卖,都直接卖电网了。价格只有夹在中间,内部市场才能吸引双方参与。这是验证模型正确性的一个快速检查点。
4.2 步长、初始价格和储能参数的影响
调参是分布式博弈仿真最花时间的地方。先看步长 \alpha。我最初设 \alpha=0.5,结果总不平衡量完全发散,\lambda 在上下界之间来回弹跳,看起来像一条疯狂抖动的折线,完全没法用。把 \alpha 降到 0.05 之后,曲线立刻稳定,大约 80 轮收敛。后来又试了递减步长公式 \alpha_k = 0.05 / (1 + k/50),收敛速度比固定步长快不少,而且不太需要提前知道合适的步长量级。这是我在多个算例里验证后觉得性价比最高的方案。
再看初始价格。初始 \lambda 设置为 \pi_{buy}=0.8 通常比设成 \pi_{sell}=0.3 更快收敛。原因是初始价格偏高时,产消者倾向多卖电(买方占便宜),价格会逐步向下调整;而初始价格偏低时,大家都抢着买电,价格快速拉升,虽然最后也能收敛,但前期调整幅度较大,振荡次数更多。
储能参数的影响也很明显。如果把储能容量从 5 kWh 提高到 10 kWh,中午富余的光伏电量可以被更多储存到晚上用,内部共享的总交易量会下降,因为“自产自用”比例提高了。这里有个经济直觉:储能本质上是把电量在时间轴上搬移,而能量共享是把电量在空间轴上搬移,两者存在替代关系。储能容量越大,产消者越不需要依赖邻居,共享收益边际递减。
4.3 两个独门技巧和一次踩坑记录
这里分享两个我在复现类似课题时反复用到的技巧。
第一个技巧是多初值验证均衡。非合作博弈的纳什均衡不一定唯一,不同初始价格可能收敛到不同均衡。我的做法是写一个外层循环,让初始 \lambda 分别取 \pi_{sell}、\pi_{buy}、两者的中值、以及几个随机值,分别跑一轮完整迭代,最后对比各初值收敛到的最终价格和交易量。如果结果一样,说明这组参数下均衡唯一性比较好;如果不一样,说明存在多个均衡,这时候论文里要如实说明,并给出当前初值下的均衡,同时讨论多重均衡对结果解释的影响。
第二个技巧是判断收敛不要只看价格,要看总量平衡。价格可能在迭代后期变化很小,但每次交易量还有明显波动;反过来,价格在上下界边界附近时,即使交易量不平衡,价格也可能不再变化。因此最稳妥的判据是同时看价格变化和总不平衡量两个指标,只要总不平衡量的无穷范数小于阈值,就认为市场出清。我在框架里用的是不平衡量判据,实践下来比纯价格变化判据更可靠。
踩坑记录:有一次我调参时发现 A 的 SOC 曲线特别怪,白天一直冲到顶,晚上又一直放到底,毫无缓冲。查了半天发现是 SOC 递推约束里放电效率除以系数搞反了,\eta_{dis} 本来应该放在分母位置,我写成了乘上去,导致仿真里放电比实际“更省电”,储能就想当然地拼命放电。这种错误用肉眼盯公式不一定看得出来,最好用 1 时段单步验证,把储能 SOC 手算一遍对比,问题马上暴露。
5. 常见问题速查与避坑清单
5.1 不收敛、无解、结果可疑:一张表快速排查
把我在调试中遇到过、以及学生和朋友问过最多的问题整理成表格,基本都是最常见的情况,可以直接对照排查。
| 症状 | 可能原因 | 处理办法 |
|---|---|---|
| 价格在上下界间来回弹跳,总不平衡量一直震荡 | 步长 \alpha 过大 | 调小 \alpha,或改用递减步长公式 |
| 迭代几百轮仍未收敛,变化趋势缓慢 | 步长 \alpha 过小或阈值过严 | 增大 \alpha 到 0.1 左右;适当放宽 \varepsilon |
| 某个产消者 quadprog 提示无可行解 | 功率平衡约束或 SOC 限值冲突 | 检查负荷/光伏数据是否合理,放宽 SOC 范围或 Pmax |
| SOC 曲线长时间顶在 0.9 或贴在 0.1 | 储能容量过小或功率限制过紧 | 增加容量,或者调整初始 SOC 到 0.5 |
| 共享价格收敛到电网售电价 \pi_{sell} 附近且市场交易量很小 | 内部市场参与动力不足 | 检查买卖价差是否够大;价差太小时能量共享本身就收益有限 |
| 结果中出现明显的同时充放电 | 没有加 Pch+Pdis \leq Pmax 约束 | 加上该不等式,或者目标中增加损耗惩罚系数 |
5.2 怎么确认我求到的是纳什均衡
这是审稿人和评委最常问的一个问题,也是代码实现后最应该补的验证步骤。严格验证纳什均衡的方法是“单方面偏离检验”:把迭代收敛得到的策略作为候选均衡,取出其中一个产消者 i,固定其他所有产消者的策略不变,重新求解产消者 i 的局部优化问题,看它的目标函数值是否比均衡状态下更低。如果更低,说明它在均衡下有偏离动机,当前结果不是纳什均衡;如果所有产消者都检验通过,没有任何单方面偏离能降低成本,那就是有效的纳什均衡。
这个检验在代码里实现并不复杂,把其他产消者的 x 固定到均衡值,产消者 i 的局部问题里共享价格要重新计算,因为价格会随交易量变化。要注意的是,在非合作博弈框架里,单纯的固定价格检验不够全面,更严格的检验需要同步考虑价格变化后的最优响应,但作为工程验证,固定价格加固定他人策略的检验已经能捕捉大部分问题。
5.3 如果想把代码继续扩展,可以从哪里入手
这套框架的扩展性相当好,我梳理几个特别值得做的方向。
第一个是换求解器,把当前“平台更新价格 + 各主体线性规划”的框架换成真正的分布式交替方向乘子法(ADMM),通过引入辅助变量把市场出清约束松弛到目标函数里,收敛速度通常比价格迭代法快,尤其在产消者数量很大的情况下优势明显。
第二个是增加配电网网络约束。当前模型假设内部共享网络是无损耗的虚拟网络,真实台区要考虑线路潮流限制、电压偏差,这就需要在迭代中嵌入一个电力潮流模块,比如用 DistFlow 潮流模型,产消者之间的交易量要满足线路容量约束。这会显著增加模型复杂度,但更贴近工程实际。
第三个是考虑光伏不确定性。当前模型用的是确定性光伏曲线,真实光伏出力受天气影响波动很大。可以用场景法生成多组光伏场景,在目标函数里取期望值,或者用鲁棒优化处理极端场景。注意,加入不确定性之后,每个产消者的局部问题会从线性规划变成带期望值的目标,求解时需要把场景展开,变量维度会成倍增加。
第四个是把单一共享价格扩展为分层结构,比如“台区内部聚合商+上层电力市场”的双层模型,产消者先跟本地聚合商交易,聚合商再跟外部市场交互,这就自然引出 Stackelberg 博弈或多层博弈,研究价值更高,但实现复杂度也上一个台阶。
最后说点实际体会
这套代码我从最初的概念模型到跑通完整仿真,前前后后改了七八版。最大的体会是,这类分布式博弈课题真正的难点不在某个产消者单体的二次规划怎么解,而在于把“耦合的市场出清条件”从集中式格式改写成分布式可迭代的格式,并让这个迭代在实际数值计算中稳定收敛。许多算法论文里一笔带过的“通过迭代达到均衡”,落到 Matlab 里可能就是一整天调步长、改投影、查约束符号的活。
我自己的经验是,一定要先把小规模场景跑稳,三个产消者、24 个时段,用固定价格手动验证单主体优化结果是正确的,再上分布式迭代。每步中间量都打出来看一遍,不要等最后出图。这样看起来慢,实际上是最快的排查路径。希望这份分享能让你少踩几个我踩过的坑。