资讯详情 微网双层优化与KKT转换:从博弈建模到MATLAB实现
📅 2026/10/22 19:42:35
1. 从单层优化到双层优化多出来的那层到底在哪如果你在微网调度这个圈子里待过一阵子大概率先接触的是单层优化给定负荷曲线给定从上级电网买电的分时电价然后让储能、光伏、可控机组老老实实地算出一个成本最低的调度方案。这套模型的好处是干净、好解、结果稳定几乎是所有入门教材的标配。但真到了实际项目里只要涉及动态电价或者需求响应单层模型就会露出一个非常尴尬的破绽——电价和负荷是互相咬合的你没法把一头固定住去优化另一头。我做的这个微网场景里运营商手里握着一套储能还带着一个可以跟上级电网交易的联络线。如果我只按单层模型去优化那就等于默认用户在任何电价下都会保持同一个用电行为。可现实是用户对电价有反应电价高了他们削减用电或者把负荷挪到别的时段电价低了他们可能多开空调、多充电。你上层把电价当作外生参数下层又把负荷当作外生参数这个循环根本闭环不了。真正的问题是运营商的定价决定用户的用电量用户的用电量又反过来决定运营商的最优调度这是一个典型的博弈过程而不是一个单纯的最优化过程。这就是双层优化模型存在的理由。学术界一般叫它领导者-跟随者结构也就是斯坦克尔伯格博弈。微网运营商作为领导者先抛出自己确定的售电价用户作为跟随者在看到电价之后按照自己的用电效用去决定买多少电。运营商在决策时不能强迫用户接受任何用电量只能通过调整电价来间接遥控用户的反应。这个关系写成数学形式是这样的min F(x, y) s.t. G(x, y) ≤ 0 y ∈ argmin { f(x, y) : g(x, y) ≤ 0 }看起来只是在上层约束里多了一个 y 的 argmin但这个 argmin 才是整个问题的灵魂。它代表用户拥有自己的优化目标上层不能替用户做主。我第一次接触这个式子时很不以为然觉得无非就是把下层问题的解用一个函数带进去结果查了半天文献才发现这件事远比想象中麻烦。下层对不同的 x 会有不同的最优反应 y(x)这个反应函数往往是非线性的你根本没办法直接把 y 显式解出来塞回上层。必须换一种思路处理下层最优性这个约束。我建议任何一个准备做微网双层优化的同学先别急着写代码第一步就是反复确认自己到底能不能讲清楚上层有哪些决策变量、下层有哪些决策变量、哪几个变量是两端共用的耦合变量。如果这三样说不清楚后面所有的MATLAB代码都只是在堆砌错误的约束。2. 上下层模型怎么落笔变量、目标、约束的边界划分我的算例规模刻意控制得比较小24个时段3个用户聚合体1套储能外加一条上级电网联络线。为什么要这么小因为双层优化模型从建立到验证需要反复修改算例太大每次调试都在等求解器跑半天心态很容易崩。等模型验证可信了再把用户数从3个扩到30个、时段从24变成96都不难。小算例不是偷懒是为了留出快速试错的余地。2.1 下层用户问题购电费与用电效用的权衡下层用户的目标说白了就是一句话在电价确定的前提下找到让自己最舒服的用电量。但最舒服不能拍脑袋定义我采用的是二次凹效用函数U_i(Q) a_i * Q - 0.5 * b_i * Q²其中 Q 是用户购电量a_i 反映用户对电力的基础依赖程度b_i 反映边际效用递减的速度。用户实际面对的问题是min π * Q - U_i(Q) s.t. Qmin_i ≤ Q ≤ Qmax_i这里 π 是运营商设定的售电价Qmin 和 Qmax 分别是需求响应协议允许的最小、最大用电量。减去效用函数是因为用户要同时权衡用电带来的满足感和需要付出的电费只有二者的差值最小化时才是用户的利益最大点。这个模型最重要的数学性质是目标函数为严格凸二次函数约束条件只有简单的区间约束因此下层是一个严格凸二次规划。严格凸意味着KKT条件不只是必要条件它同时也是充分条件KKT点就是全局唯一最优解。这个性质在后面做KKT转换时起了决定性作用如果下层问题不够凸后面所有推导都会站不住脚。2.2 上层运营商问题定价、储能调度与配网购电上层运营商的决策变量有三组各时段的售电价 π(k)、储能净放电功率 P_ess(k)、向上一级电网的购电功率 P_grid(k)。P_ess 为正时表示放电为负时表示充电。运营商的日净收益表达式为max Σ [ π(k) * Q_total(k) - c_buy(k) * P_grid(k) - c_ess * |P_ess(k)| ]Q_total(k) 是当前时段所有用户购电量之和也就是微网总负荷c_buy(k) 是运营商向配电网购电的分时电价c_ess 是储能单位充放电损耗成本。加这个很小的损耗项目的不只是经济性更重要的是避免储能模型在目标函数中无成本地反复充放电生成一些震荡解。上层约束更多是常规项功率平衡约束 P_grid P_ess P_pv Q_total储能SOC递推式 SOC(k1) SOC(k) - P_ess(k)*Δt/E_capSOC上下限约束储能功率限值售电价上下限联络线购电功率上限。这些约束在单层调度里都见过真正的难点在第2.3节的耦合关系上。2.3 上下层耦合点与参数量级预算上下层的第一个耦合点是售电价 π(k) 出现在下层用户的目标函数里。第二个耦合点是用户总购电量 Q_total(k) 同时出现在上层的功率平衡约束和目标函数中。这两个耦合点会让整个模型形成闭环上层定价 → 用户响应 → 用户响应结果反哺上层收益与功率平衡。在设计模型阶段还有一个容易被忽略的工作是提前估算KKT线性化需要的M值。电价量级大约在0.3到1.5元/千瓦时用户购电量通常在几十千瓦的量级效用系数 a_i 为了让用户目标函数在量级上合理也要取到几元/千瓦时b_i 则一般在10的负二次方到负三次方之间。粗略估算下来KKT条件中拉格朗日乘子的量级通常在几十以内预留M取500左右是安全的。这个估算看似随手实际帮我省掉了大量后期数值调试的时间我见过太多人上来就M取一万结果模型数值病态到求解器根本没法收敛。3. 求解方案选型为什么KKT转换最终成了我的选择双层优化不是只有一个解法。我在动手之前梳理了三条最主流的路线也分别试跑过最终才确定KKT转换这条路线最稳妥。3.1 三条常见路线对比求解路线基本思路优点缺点适用场景枚举/离散化把上层变量按网格离散对每个组合求解一次下层实现简单不需要推导组合爆炸只能得到近似解上层变量少、精度要求低的演示算例智能优化算法外层用遗传/粒子群搜上层变量内层每次求解下层几乎不受模型非线性限制目标函数不光滑容易陷入局部最优且无最优性保证非线性强、精度要求不高的探索KKT/强对偶转换把下层问题的KKT条件作为约束并向上层合并可转成严格MILP求解器给出全局最优推导工作量大互补松弛线性化易出错下层为凸、约束连续的典型微网双层模型我自己最初用枚举法跑过一个1时段1用户的极简算例效果不错甚至能画出电价-收益曲线。但我知道一旦把上层电价做成24时段连续变量枚举的复杂度直接指数爆炸。智能算法我也跑过粒子群搜电价、内层用quadprog求用户响应问题在于上层目标函数不是光滑函数粒子群很容易在一个局部鞍点附近反复横跳且完全无法证明解的最优性。最终我选择了KKT转换路线。理由有三第一下层问题是严格凸二次规划KKT条件完全等价于下层原问题第二互补松弛经过大M线性化后可以转化为MILP成熟的商业求解器能给出带最优性保证的全局解第三只要推导一次后续更换场景、增加约束都只需要在模型里追加条件不需要换求解框架。3.2 KKT转换的完整推导与线性化逻辑对每个下层用户 i、每个时段 k写下拉格朗日函数并对 Q 求导得到稳定性条件a_i - b_i * Q_i(k) - π(k) - λ_i_ub(k) λ_i_lb(k) 0λ_i_ub 对应上限约束 Q_i ≤ Qmax_i 的乘子λ_i_lb 对应下限约束 Qmin_i ≤ Q_i 的乘子。与此同时还要满足互补松弛条件λ_i_ub * (Qmax_i - Q_i(k)) 0 λ_i_lb * (Q_i(k) - Qmin_i) 0这两个等式里带乘积项是非线性约束不能直接扔给MILP求解器。需要用大M法引入0-1变量 z把它拆成两组线性约束λ_i_ub(k) ≤ M * z_ubQmax_i - Q_i(k) ≤ M * (1 - z_ub) λ_i_lb(k) ≤ M * z_lbQ_i(k) - Qmin_i ≤ M * (1 - z_lb)这组约束等价于两句话要么乘子非零且约束取等号要么约束放松且乘子为零。换句话说z 是该约束是否被激活的开关。很多初学者把第二行写成 Q 减去上下限的方向结果方向一错模型不报错但解出来的互补松弛完全不成立。这种事真的需要一个一个小算例去验证不能指望求解器帮你发现逻辑错误。3.3 为什么我不建议直接用现成的bilevel求解器很多人看到YALMIP自带bilevel solver接口就很兴奋我一开始也一样。但实测发现这个接口限制太多下层必须能写成特定的二次规划形式约束结构稍有变化就报错而且报错信息对调试没有任何帮助。更关键的是使用现成bilevel求解器意味着你把模型的推导过程外包给了黑箱一旦结果不合理你甚至不知道是模型错了还是求解器内部出了问题。自己手动做KKT转换虽然前期费些时间但每一步都能掌握清楚后续构建不同场景时也能灵活修改约束条件。如果你实在想验证自己推导是否正确可以先用一个极小规模的算例把用KKT转化后的MILP解和暴力枚举解做对比一致了再放心放大规模。4. Matlab代码骨架从零把一个双层模型搭成可求解的MILP到代码部分我不打算把整个工程几百行全部贴出来那样太长且针对性太强。我更想把骨架结构讲清楚尤其是那些容易写错的细节。整体文件组织可以分成三块。4.1 三个文件的结构与YALMIP建模优势case_data.m集中定义所有参数包括时段数、用户数、储能容量与功率限值、分时购电价、光伏出力序列、用户效用系数、用电上下限以及上一步估算好的M值。build_model.m核心建模函数输入参数结构体 params输出YALMIP的决策变量、约束集合和优化目标。main.m主脚本依次执行载入参数、构建模型、配置求解器、求解、画图。我之所以坚持用YALMIP而不是直接用原生求解器接口是因为YALMIP允许你以矩阵形式声明变量和约束建模效率高而且以后想从Gurobi切换到CPLEX或者其他求解器只需要改一处求解器设置。唯一要付出的代价是YALMIP在建模预处理时有一点性能开销对中小规模微网算例来说完全无感。4.2 核心代码KKT条件、储能SOC与目标函数build_model.m 里最核心的变量声明部分是这样的function m build_model(p) N p.N; I p.I; pi_sell sdpvar(N, 1); % 上层变量各时段售电价 P_ess sdpvar(N, 1); % 上层变量储能净放电功率 P_grid sdpvar(N, 1); % 上层变量向上级购电功率 Q sdpvar(N, I); % 下层用户购电量也是上下层耦合变量 lam_u sdpvar(N, I); % 上限约束乘子 lam_l sdpvar(N, I); % 下限约束乘子 z_u binvar(N, I); % 上限互补松弛的0-1指示 z_l binvar(N, I); % 下限互补松弛的0-1指示然后写KKT稳定性条件和互补松弛约束cons []; for k 1:N for i 1:I cons [cons, p.a(i) - p.b(i)*Q(k,i) - pi_sell(k) ... - lam_u(k,i) lam_l(k,i) 0]; cons [cons, lam_u(k,i) 0, p.qmax(i) - Q(k,i) 0, ... lam_l(k,i) 0, Q(k,i) - p.qmin(i) 0]; % 互补松弛线性化λ_u0 当且仅当 Q取到上界 cons [cons, lam_u(k,i) p.M(k,i) * z_u(k,i), ... p.qmax(i) - Q(k,i) p.M(k,i) * (1 - z_u(k,i))]; % 互补松弛线性化λ_l0 当且仅当 Q取到下界 cons [cons, lam_l(k,i) p.M(k,i) * z_l(k,i), ... Q(k,i) - p.qmin(i) p.M(k,i) * (1 - z_l(k,i))]; end end注意上面 p.qmax(i) - Q(k,i) 0 这一行约束是用户问题本身的上限约束不能省略。有些简化版本只写KKT稳定性条件和互补松弛约束把原始可行性约束漏掉了模型照样能出解但解出的Q可能根本不在用户允许范围内。接着写储能SOC递推和功率平衡SOC sdpvar(N, 1); cons [cons, SOC(1) p.soc0 - P_ess(1)*p.dt/p.ess_cap]; for k 2:N cons [cons, SOC(k) SOC(k-1) - P_ess(k)*p.dt/p.ess_cap]; end cons [cons, p.soc_min SOC p.soc_max, ... -p.ess_pmax P_ess p.ess_pmax]; Qsum sum(Q, 2); cons [cons, P_grid P_ess p.p_pv Qsum]; % 功率平衡 cons [cons, p.pi_lb pi_sell p.pi_ub]; % 电价限值 cons [cons, 0 P_grid p.grid_pmax]; % 联络线功率限值储能递推式这个方向很多人会搞混P_ess 为正表示放电SOC 应该下降所以是减去 P_ess*Δt/E_cap如果写成加号储能就会一边放电一边SOC上升模型结果看起来风平浪静实际上物理过程完全拧了。目标函数方面YALMIP默认最小化所以上层收益最大化要写成最小化负收益ess_abs sdpvar(N, 1); % 储能损耗的绝对值线性化 cons [cons, ess_abs P_ess, ess_abs -P_ess]; m.cons cons; m.obj -( sum(pi_sell .* Qsum) ... - sum(p.c_buy .* P_grid) ... - p.c_ess * sum(ess_abs) ); m.pi_sell pi_sell; m.Q Q; m.P_ess P_ess; m.P_grid P_grid; end4.3 主脚本与求解器参数设置main.m 的调用逻辑非常直接case_data; m build_model(params); ops sdpsettings(solver, gurobi, mipgap, 0.01, verbose, 2); result optimize(m.cons, m.obj, ops); if result.problem ~ 0 error(求解失败: %s, result.info); end这里有一个很关键的求解器经验MILP的默认MIPGap通常设到1e-4但对带大M约束的模型这个值会让求解器花费大量时间去证明一个本身就带数值噪声的整数解达到极小gap。我实际测下来MIPGap放宽到1e-2求解时间可以下降一个量级而结果质量几乎不受影响。如果你机器上没装GurobiYALMIP也能调用MATLAB自带的intlinprog只是算例规模稍大时速度会明显变慢建议还是装一个免费的学术版Gurobi或CPLEX。5. 调试期中让我反复折腾的五个细节从模型第一次跑通到结果真正可信我花了将近三周。这中间踩过的坑每一个都值得单独写一段。5.1 大M的选择与数值病态我第一次图省事把所有M统一设成10000。结果是Gurobi报numerical difficulties解出来的值看起来合理但对账时发现互补松弛条件被数值误差彻底穿透用户的最优性条件实际上不再成立。后来我把M改成按用户和时段分别估算总体控制在500的量级模型立刻稳定下来。M取值没有通解核心思路是先通过线性松弛求一次乘子范围的估计值再留出3到5倍的裕量。这个方法虽然笨但非常有效。5.2 互补松弛线性化的或逻辑不能写反线性化时很多初学者会写成 λ ≤ Mz 且 g ≤ Mz认为g不为0时z必须为1但这是错的。正确的配对必须是 λ ≤ Mz 与 h ≤ M(1-z)规定z1时h被压到0z0时λ被压到0。这个方向问题模型不会报错但会静悄悄地破坏KKT条件。我排查这个问题的方式是搭一个两时段的微算例把MILP结果和暴力枚举结果逐步对比最后才定位到某一行约束方向写反。5.3 下层目标不够凸KKT转换就失去严谨性一开始我把用户效用写成线性函数 U_i a_iQ下层问题退化成线性规划。线性规划的最优解不一定唯一这就意味着上层在KKT点处无法确定下层用户到底选取了哪一个最优响应模型的整体结果不可复现。我的解决办法是在用户目标函数里加一个很小的正定二次项 0.5ε*Q²ε取1e-4。它不影响实际最优解位置但能让下层问题重新变回严格凸优化KKT转换的充要性也就恢复了。5.4 量纲换算会悄无声息地毁掉结果我有一版模型用的是96时段、每时段15分钟结果收益数值比预期大出4倍。排查到最后才发现所有费用项都忘了乘时间段长度Δt。在24时段、Δt1小时的情况下这个bug完全隐身一改成15分钟时段收益和SOC递推式同时出错。从那以后我把Δt单独定义在参数文件里所有费用项统一写成功率×Δt×电价绝不在公式里写裸的功率乘电价。这个习惯直接杜绝了同类问题。5.5 用暴力网格搜索做小型对照实验每个双层优化模型我都会保留一个对照组取1个时段、1个用户把电价从0.3到1.5按步长0.01穷举一遍每个电价用quadprog求用户的最优Q再算上层收益。画出电价-收益曲线后把MILP解出来的结果点投到曲线上看是否吻合。这个方法成本极低却能在十分钟内发现KKT推导、方向或M值中的大部分问题。我强烈建议大家都保留这个验证步骤它比看任何求解器日志都直观。6. 结果曲线里的博弈特征以及我总结的三层进阶第一批结果图出来后最有意思的现象是售电价曲线并不是简单地贴着购电价上边界走。高峰时段的售电价会略高于购电价低谷时段的售电价会被压得很低甚至靠近下限储能则顺着电价差在低谷充电、高峰放电。部分时段的定价恰好落在再提一点价用户就会明显削减用电的临界点附近这就是双层博弈均衡在曲线上的具体表现。对比固定售电价的单层调度双层模型算出的运营商总收益要高一些而且高出的部分不是靠单纯抬价而是来自储能套利和负荷削峰填谷带来的整体效率提升。这个结果本身也印证了做双层优化的价值不考虑用户响应侧的调度容易高估定价的自由度运营商以为价格越贵越好实际在需求弹性面前最优价格往往是那个让用户愿意买又不至于买太多的平衡点。回看整条实现路线我把自己的经验归纳成三层进阶。第一层是能说清楚上下层各自的决策变量、收益边界和耦合变量第二层是能独立完成KKT推导并理解每个线性化约束背后的互补语义第三层是能设计小规模对照实验验证模型结果在逻辑上和数值上都是可信的。这三层只要有一层含糊后面的Matlab实现就很容易变成看起来很能跑、实际结果经不起推敲的假仿真。最后再分享一个让我受益很久的工程习惯所有参数从第一天起就用结构体绑在一起传参而不是在脚本里散落一堆全局变量。我用结构体之后从3个用户改成30个用户、从24时段改成96时段只需要重写参数文件build_model函数本身一行都不用动。如果你现在刚开始写自己的双层优化模型这一个习惯能帮你省下后续无数次重构的功夫。
相关阅读
10分钟快速上手:qiaomu-opencli-skills安装、浏览器扩展与第一个命令完整教程
AI 技能人工智能 【免费下载链接】qiaomu-opencli-skills Use opencli CLI to interact with social/content websites (Bilibili, Zhihu, Twitter/X, YouTube, Weibo, 小红书, V2EX, Reddit, HackerNews, 雪球, BOSS直聘 etc.) 项目地址: https://gitcode.com/gh_…
2026/10/22 20:42:53 阅读全文 →