1. 为什么单微网不够用:多微网电能互补的工程背景
1.1 单微网调度的天花板
我最早接触微网优化的时候,其实是有个疑惑的:单微网里光伏、储能、燃气轮机该有的都有了,为什么非要跟其他微网互联,这不是徒增复杂度吗?直到我把一套园区微网的真实数据跑完才明白,单微网的调度空间是有限的。
以典型的光伏+储能微网为例,光伏出力曲线和负荷曲线天然错位,中午光伏大发但负荷可能只有一小半,晚上负荷上来了光伏又归零。储能呢,一个400kWh的电池,按每天一充一放算,能覆盖的削峰时段也就两三个小时,遇到连续阴天更是捉襟见肘。剩下的缺口只能向配电网买电,而配电网执行的是峰谷分时电价,傍晚高峰段的购电成本高得肉疼。我当时算出来的结果是,中午被迫弃掉的光伏电量占光伏总发电量的两成多,晚上又花高价从配网购电补缺口——白白交了冤枉钱。
这就是单微网的瓶颈:资源禀赋固定,调节手段有限,跟配电网的交互又是“单向输血”,没有横向互济的通道。光伏富余时段只能弃,负荷高峰时段只能买,成本压不下来。
1.2 多微网互联带来的“化学反应”
把几个特性互补的微网通过联络线连起来,调度空间立刻就不一样了。举个例子:微网A屋顶光伏装得多,中午发电用不完;微网B商业负荷为主,白天刚好是用电高峰;微网C有台工业余热发电机组,但夜间负荷低、机组不能随便启停。这三个微网要是在物理上互联,A中午的富余光伏可以直接卖给B,C夜间的富余电量可以存到A或B的储能里,A缺电的晚上可以从B的储能取电。整体算下来,向配电网的高峰购电量显著下降,弃光率也能从20%压到个位数。
电能互补的本质,就是让每个微网不再孤军奋战,而是把彼此的可再生出力波动、负荷曲线差异、储能冗余能力都当作系统的调节资源来用。这和电网层面的区域互济是一个道理,只不过微网层面的决策粒度更细、约束更多——每个微网内部有自己的设备约束和利益诉求,不能简单粗暴地当成一个整体来调度。
1.3 需求响应在这张图里的位置
多微网互联解决了“电源与负荷的空间错配”,需求响应要解决的是“用电行为的时间错配”。用户侧负荷并不是完全刚性的,适当的经济激励可以让一部分负荷平移或削减。我常说需求响应相当于给调度员手里加了一把“软调节”的旋钮:高价时段让可转移负荷挪到低价时段,可削减负荷直接砍掉一部分,代价是付给用户一定的补偿费用。
在多微网背景下,需求响应的价值是双重的。对内,它降低了单个微网的峰值购电需求;对外,它缓解了微网间联络线的拥塞,让电能互补通道在高峰时段不至于被阻塞。这两个机制叠加,系统总成本的下降幅度往往超出预期,这也是很多论文把需求响应和多微网互补放在一起研究的原因。
2. 双层优化为什么适合这个问题:角色划分与模型框架
2.1 上层决策者:站在全局的调度中心
多微网互联之后,第一个绕不开的问题是:谁来定规则?如果所有微网都是同一个运营商的资产,那可以直接写一个超大单层优化模型,把所有决策变量放一起,求全局最优就行。但现实里,园区微网、商业楼宇微网、工业微网往往分属不同业主,各有各的利益诉求,调度中心不能大包大揽替它们做所有决定。
这时候就得给上层一个鲜明的角色定位:它掌握全局信息,负责制定微网间的交互电价(或者交互功率计划),目的是让整个多微网系统的总运行成本尽可能低。上层不做具体设备的启停决策,它只发“经济信号”,比如告诉微网A和微网B:“这个时段你们之间的交易电价是每度0.45元”。
2.2 下层决策者:各微网的本位主义
每个微网都是“经济理性人”。给定上层下发的电价信号后,下层只关心自己的利益最大化——本地运行成本最小化,包括燃气轮机燃料费、从配网购电费、需求响应补偿费,再减去向配网售电和向邻居微网售电的收入。
恰恰是这种“本位主义”,构成了双层模型的本质特征。同一个调度问题,单层优化里每个微网的目标都服从全局最优;双层优化里每个微网的目标是各自最优,全局最优要通过经济信号去诱导。比如上层想让微网A中午多送电给B,就得把交互电价定得让A觉得“卖出去比我存着更划算”,同时让B觉得“买进来比我自家发电更划算”。电价定得太低,A不卖;定得太高,B不买。这本质上就是一对矛盾,谁也不能靠命令解决。
这里要特别说明一个容易混淆的点:很多同学一听“博弈”就以为一定要写分布式迭代算法,反复交换数据直到收敛。其实在微网调度这个场景里,多数论文采用的是单次集中式求解——把下层的最优反应行为用KKT条件“嵌入”上层问题,一次性求出主从博弈的均衡解。模型上用双层,解法上可以一步到位,这也是本文实现的核心思路。
2.3 上下层之间的衔接变量
双层模型有两个关键衔接桥梁:一个是价格信号,上层决策的交互电价会进入下层的目标函数;另一个是功率计划,下层在上层给定的电价下决策出微网间交互功率和自购/自发电策略,再反馈回上层评估整体目标。在上层目标函数里,微网间的电费结算属于内部转账,相加后互相抵消,所以上层看到的是实实在在的燃料费、配网购电费和需求响应补偿费。这样的结构可以避免“电价×功率”双线性项钻进系统总成本,给后面的线性化求解省了很大麻烦。
3. 数学建模:功率交互、储能约束与需求响应成本
3.1 微网内部设备模型:先把“家底”写清楚
建立双层模型前,先把每个微网内部的设备约束写成数学表达式。这里用到的都是标准的混合整数线性规划(MILP)约束,核心的四类设备如下。
燃气轮机的运行成本通常压成线性或分段线性函数:
- 出力上下限:P_g_min ≤ P_g(t) ≤ P_g_max
- 爬坡约束:-ΔP_down ≤ P_g(t) - P_g(t-1) ≤ ΔP_up
- 运行成本:C_g(t) = a·P_g(t) + b,如需更精细可以分三段线性化
储能的动态模型是微网调度里最容易写错的部分,正确的递推式是:
SOC(t+1) = SOC(t) + (η_ch·P_ch(t) - P_dis(t)/η_dis)·Δt / E_cap
同时还要配套约束:
- 充电功率上限:0 ≤ P_ch(t) ≤ P_ch_max·u_ch(t)
- 放电功率上限:0 ≤ P_dis(t) ≤ P_dis_max·u_dis(t)
- 状态互斥:u_ch(t) + u_dis(t) ≤ 1
- 容量边界:SOC_min ≤ SOC(t) ≤ SOC_max
u_ch、u_dis是0-1变量,用来保证储能不会同时充电和放电。这个互斥约束看着不起眼,实际建模时非常关键,如果不加,优化器经常会算出“边充边放”的荒谬结果。
3.2 电能互补的联络线建模
微网之间功率交互的建模相对直接。定义变量P_ex(i,j,t)表示t时段微网i从微网j购入的功率,那么联络线约束至少要包含:
- 传输容量上限:0 ≤ P_ex(i,j,t) ≤ P_ex_max(i,j)
- 方向互斥:P_ex(i,j,t)·P_ex(j,i,t) = 0,避免同一联络线上同时双向送电
- 网损可以暂时不计,或者按传输功率的固定比例折算
每个微网的功率平衡是整个模型的骨架:
P_pv(t) + P_g(t) + P_dis(t) + P_buy_grid(t) + Σ_j P_ex(i,j,t) = P_load(t) - ΔP_DR(t) + P_ch(t) + P_sell_grid(t) + Σ_k P_ex(k,i,t)
左端是电源侧各出力的总和,右端是负荷侧需求。ΔP_DR(t)是需求响应削减量,放在右端相当于降低了等效负荷。这条约束必须对每个微网、每个时段都成立,是连接所有变量的“总线”。
3.3 需求响应建模:激励型和价格型怎么选
需求响应建模主要有两条路。激励型(IDR)比较适合日内调度:直接定义一个可削减负荷变量ΔP_DR(t),配上限约束和补偿成本。
- 削减量上限:0 ≤ ΔP_DR(t) ≤ γ·P_load(t),γ一般在0.05到0.15之间
- 补偿成本:C_DR(t) = c_DR·ΔP_DR(t)
价格型(PDR)则是通过弹性矩阵预测负荷对电价的响应,常用在较长时间尺度的分析里。ΔP_load(t) = P_load_base(t)·Σ_k E(t,k)·Δρ(k)/ρ0(k),其中E(t,k)是电价弹性系数矩阵,对角元是自弹性(负值),非对角元是交叉弹性(正值)。PDR的数据要求高,日内优化里用得少,我自己的项目里以IDR为主,PDR只在扩展分析中做了一组对比。
3.4 目标函数怎么定:上下层各有各的算盘
下层目标函数(微网i的运行成本最小化):
min Σ_t [C_g_i(t) + C_DR_i(t) + π_grid_buy(t)·P_buy_grid_i(t) - π_grid_sell(t)·P_sell_grid_i(t) + Σ_j π_ex_i_j(t)·P_ex_i_j(t)]
注意π_ex_i_j(t)是交互电价,站在下层视角它由上层给定,是已知参数;站在整体模型视角,它和P_ex都是决策变量,这就容易生成双线性项,后面专门讲化解办法。
上层目标函数(区域总成本最小化):
min Σ_t Σ_i [C_g_i(t) + C_DR_i(t) + π_grid_buy(t)·P_buy_grid_i(t) - π_grid_sell(t)·P_sell_grid_i(t)]
两组目标看起来很像,但关键差异在最后一项:上层目标里没有微网间的电费结算项。因为A付给B的电费,就是B从A收到的电费,在系统总账上互相抵消了。这个设计不仅符合“调度中心追求总成本最优”的定位,还避免了上层目标中的双线性项,是建模时需要特别留意的一步。
4. 求解路线的分岔口:KKT转化与智能算法选哪个
4.1 KKT转化:把下层的“理性”变成上层的约束
先明确一点:下层问题是一个线性规划(LP),在Slater条件下,它的最优解与KKT条件完全等价。于是我们可以把下层LP的KKT条件作为一组约束塞进上层问题,双层优化就转化成了单层优化。
KKT条件分成四类:
- 稳定性条件(Stationarity):目标函数对每个决策变量的导数加上各约束带拉格朗日乘子的导数,求和等于零
- 原始可行性(Primal feasibility):下层模型自己的全部约束
- 对偶可行性(Dual feasibility):所有乘子大于等于零
- 互补松弛(Complementary slackness):每个乘子与对应的不等式约束左边乘积等于零
前三个条件都是线性的,麻烦的是第四个——“乘积等于零”是非线性约束。好在工程上有标准处理办法:引入大M和二进制变量,把它线性化。
4.2 大M法:互补松弛条件的线性化
对每一对互补条件 a ≥ 0, b ≥ 0, a·b = 0,等价于引入0-1变量z之后的一组线性约束:
- a ≤ M·(1 - z)
- b ≤ M·z
当z=1时,a被迫为0,b自由;当z=0时,b被迫为0,a自由。这样“至少有一个为0”就实现了,而且全是线性不等式。
M取值是个真功夫活。取太小,最优解会被错误截断,比如本该有功率交互的时段被M卡死;取太大,数值病态严重,CPLEX/Gurobi要么收不动,要么精度差。我的做法是先解一个把互补约束全部松弛掉的LP,看各变量和乘子的最大可能量级,然后取一个比最大可能值大一个数量级的值,基本不会出大问题。
4.3 为什么放弃智能算法嵌套
不少论文喜欢用粒子群(PSO)、遗传算法(GA)嵌套线性规划来解双层模型:外层搜电价,内层用LP算微网最优响应。这个方法框架简单,不用推导KKT,对非线性目标适应性强,但我在工程里吃过亏:
- 外层智能算法参数敏感:种群规模、惯性权重、交叉概率稍稍一变,结果差十万八千里
- 没有最优性保证:跑十次八次不同结果,你不知道哪个是全局最优
- 计算量爆炸:3个微网24时段,外层每次迭代都要内层调用三次LP,跑2000次迭代相当于解6000个LP,一台普通电脑要跑很久
而本文这种LP+LP结构的双层问题,KKT转化后是一个MILP,CPLEX/Gurobi通常几秒到几十秒就能拿到全局最优解。只有在模型不得不保留非线性(比如燃气轮机二次成本不线性化)的情况下,KKT条件变成非线性互补问题,我才会考虑用智能算法或者启发式方法。
5. Matlab实现核心:YALMIP建模、大M法和求解器配置
5.1 环境配置:YALMIP加商用求解器
用Matlab实现这套模型,我的标配是YALMIP加一个商用求解器(CPLEX或Gurobi)。YALMIP是Matlab下的一个建模层,不用手工拼求解器的API,声明变量和约束的方式非常接近数学表达,调试代码时心智负担小很多。
安装完成之后,先在命令行运行一次测试确认求解器连通正常。确认之后再进主循环,能省掉后面大量“为什么我的optimize不工作”的烦恼。
5.2 变量定义与约束拼接的核心写法
下面是典型的核心代码骨架,完整项目代码会放到文末的说明中,这里先把变量定义和约束拼装思路讲清楚。
%% 双层模型转单层后的YALMIP实现框架 % 基本参数 n_mg = 3; % 微网数量 n_t = 24; % 优化时段数,默认1小时一步 % 决策变量 P_g = sdpvar(n_mg, n_t); % 各微网燃气轮机出力 P_ch = sdpvar(n_mg, n_t); % 储能充电功率 P_dis = sdpvar(n_mg, n_t); % 储能放电功率 P_ex = sdpvar(n_mg, n_mg, n_t); % 微网间交互功率 P_buy = sdpvar(n_mg, n_t); % 从配网购电 P_sell = sdpvar(n_mg, n_t); % 向配网售电 Delta_DR = sdpvar(n_mg, n_t); % 需求响应削减量 u_ch = binvar(n_mg, n_t); % 充电状态 u_dis = binvar(n_mg, n_t); % 放电状态 % 下层LP的拉格朗日乘子(KKT转化后变成上层问题的变量) lambda_bal = sdpvar(n_mg, n_t); % 功率平衡约束对偶乘子 lambda_soc = sdpvar(n_mg, n_t - 1); % SOC递推乘子(等式约束用自由乘子) mu_up = sdpvar(n_mg, n_t); % 不等式约束乘子(非负) mu_down = sdpvar(n_mg, n_t); % 不等式约束乘子(非负)然后进入约束拼装阶段,核心提醒一个问题:YALMIP的sdpvar初始化为全0矩阵,维度不对时经常不报错而是悄悄广播,导致约束维度膨胀然后求解器报“infeasible”。我处理这类问题的方式是先把变量reshape成一维确认维度,再进入约束循环。
5.3 KKT条件的代码表达
KKT转化后的MILP模型里,最核心的部分是把下层LP的stationarity条件写成等式约束。比如对变量P_ex(i,j,t),下层目标里有π_ex(i,j,t)·P_ex(i,j,t),那么stationarity条件形如:
π_ex(i,j,t) + Σ_mu·系数 - lambda_bal(i,t) = 0
这段代码建议用for循环显式生成:
C = []; for i = 1:n_mg for t = 1:n_t % 功率平衡约束对应的stationarity项 C = [C, lambda_bal(i,t) == ... % 与P_g相关的导数项 + 2*a_g*P_g(i,t) + b_g ... + mu_up(i,t) - mu_down(i,t)]; end end补充一下,KKT中的互补条件在YALMIP中可以直接用大M法矩阵化写:
% 假设g_ub是某组不等式约束左边,mu是乘子,M是充分大常数 z = binvar(size(g_ub)); C = [C, g_ub >= 0, mu >= 0]; % 原始可行+对偶可行 C = [C, g_ub <= M*z, mu <= M*(1-z)]; % 互补松弛线性化5.4 双线性项的处理
这个坑我前前后后绕了好几天。当上层决策交互电价π_ex、下层决策交互功率P_ex时,下层目标里存在π_ex·P_ex。站在下层的LP视角,π_ex是参数,没问题;但KKT转化之后,π_ex和P_ex都是单层模型里的决策变量,乘积变成双线性项,MILP求解器直接罢工。
解决方案有两种,亲测都走得通。
方案A(推荐):上层目标只用区域总成本,内部交易电费在系统总账上抵消,因此单层模型的目标函数里不会出现π_ex·P_ex。做完这一层规避之后,还要检查下层KKT条件里是否还有其他双线性项。如果下层目标函数里交互费用项是π_ex·P_ex,那对P_ex求导后得到的是π_ex本身,这是线性表达式,不会引入双线性问题。但对π_ex求导就会出现P_ex,如果上层模型里有对π_ex的stationarity条件就需要格外小心。所以我的做法是只在目标函数层面规避,约束层面逐条检查即可。
方案B:如果模型非要保留交易结算项在上层利润里,那只能用固定参考电价或交替迭代法——先给定π_ex初值,求解下层得到P_ex,再固定P_ex去优化π_ex,反复交替直到收敛。这个方案可行,但牺牲了KKT一次求解的便利性,而且要额外判断收敛条件,一般我建议非必要不用。
5.5 求解器设置与常见参数
调用求解器时,我习惯把参数显式设置在sdpsettings里:
options = sdpsettings('solver', 'cplex', ... 'verbose', 2, ... 'savesolveroutput', 1, ... 'cplex.mip.tolerances.mipgap', 0.0001); sol = optimize(C, Objective, options);mipgap设置到1e-4已经足够工程精度,太严反而拖慢求解时间。如果问题规模大,可以先放宽到1e-2跑通逻辑,验证模型没问题后再收紧。
6. 仿真结果怎么看:互补效果、负荷平移与成本对比
6.1 算例场景设计
仿真算例我设计成三个微网、24个时段。微网1是光伏富余型,装机600kW,本地负荷小;微网2是商业负荷型,白天负荷高但没有光伏;微网3是工业微网,负荷平稳,带一套500kW/1MWh的储能和一台燃气轮机。配电网购电执行峰谷分时电价,峰段1.2元/kWh,平段0.75元/kWh,谷段0.4元/kWh,余电上网收购价0.35元/kWh。
对比三种模式:
- 模式A:三个微网独立调度,不互联,无需求响应
- 模式B:多微网互联协调调度,无需求响应
- 模式C:多微网互联协调调度,加上激励型需求响应
6.2 成本与弃光数据对比
跑完优化后整理出下表,结果非常直观:
| 指标 | 模式A(独立) | 模式B(互联) | 模式C(互联+DR) |
|---|---|---|---|
| 系统总运行成本(元) | 12850 | 11120 | 10050 |
| 弃光率(%) | 22.5 | 8.3 | 5.1 |
| 配电网峰值购电功率(kW) | 1380 | 1020 | 860 |
| 需求响应补偿成本(元) | 0 | 0 | 620 |
| 总成本相对模式A降幅 | - | 13.4% | 21.8% |
从B到C,需求响应用它那620元的补偿成本,换来了超过1000元的系统成本下降。很多人会觉得需求响应的补偿是纯增加的成本,实际算过之后才知道,它削掉的高峰购电支出远超补偿费。
6.3 怎么验证“互补”真的发生了
看总成本太抽象,我习惯把微网间交互功率曲线打出来看。模式B下,微网1向微网2的交互功率在11:00-14:00出现明显的正向尖峰,这正是光伏大发时段——富余电被微网2吃掉了。傍晚17:00-20:00,微网3的储能开始放电,部分功率通过联络线送到微网2,帮助它躲过晚高峰购电。这两段交互功率曲线就是“电能互补”的直接证据。
另一个值得打印的曲线是微网1的弃光量。独立调度模式下,弃光曲线在午间有个大凸包;换成互联模式后,凸包几乎消失,说明原本被“扔掉”的光伏电量通过联络线找到了就地消纳的出口。
6.4 需求响应带来的负荷形态变化
模式C里需求响应的作用从微网2的购电曲线上看得一清二楚。原本晚高峰段的购电尖峰被削掉了一块,凌晨低谷段的购电曲线略有抬升,这是可转移负荷从高峰挪到低谷的典型表现。可削减负荷集中在17:00-20:00被削减,削减量保持在总负荷的10%以内,用户感知不明显,但系统峰值负荷实实在在地降下来了。
这里有个小提示:仿真结果出来后,一定要检查需求响应削减量有没有顶在上限。如果大量时段ΔP_DR都等于γ·P_load,说明需求响应潜力已经挖尽,再增加补偿单价也不会带来额外收益,这时就应该考虑扩容联络线或增加储能,这是模型给出的一条很有价值的边际信号。
7. 我调试这套代码踩过的坑
7.1 大M的取值曾经让我白跑一晚上
有一版代码我偷懒把M统一设成1e6,结果CPLEX跑了半个小时还在磨,解出来的结果里出现了“微网1中午光伏大量弃电,同时向配电网高价购电”的离谱现象。查了一晚上才发现是M太大导致互补条件的数值松弛,最优性判断被污染了。
后来我改成两步走:第一步把互补条件全部删掉,求一个松弛LP,统计所有乘子的最大绝对值;第二步把M设成这个最大值的10倍,重新加入约束求解。从此再没出现过类似的数值问题。这个经验分享给所有做KKT转化的朋友:M不是越大越好,够用就行。
7.2 储能SOC出现负数:单位不统一的锅
有次仿真结果里储能的SOC曲线出现负值,我先怀疑SOC递推约束写错了,检查了半天没发现问题,最后发现是单位混用了——功率用了kW,能量却用了MWh,导致SOC增量算出来差了一千倍。后来我立了个规矩:全模型功率统一用kW,能量统一用kWh,时间步长Δt统一用小时,SOC递推公式里的η_ch和η_dis也全部提前折算成标幺值。单位理清之后,这一类低级错误基本绝迹。
7.3 YALMIP常见的三个报错
我把自己遇到的报错归纳成三类,给后来人避雷。
- “No suitable solver for problem type”:问题被识别成MIQP或非线性,但求解器不支持。解决方案是回溯模型,找出产生非线性的位置,优先考虑分段线性化或大M法。
- “Index exceeds array bounds”:P_ex这种三维矩阵在循环里索引时最容易写错维度。我习惯先把三维变量按squeeze方式拆成二维逐时段处理,跑通后再优化成矩阵运算。
- objective里的NaN或Inf:几乎都是参数没赋值或者除零。解决办法是用assert检查每个参数,尤其注意E_cap这种出现在分母上的量。
7.4 调试顺序是效率的分水岭
我见过太多同学一上来就搭完整的三微网24时段KKT模型,跑不通之后对着几千行代码发呆。我的经验是严格按“1微网1时段 → 1微网24时段 → 2微网24时段 → 3微网24时段+DR”的顺序递进调试。每一步都能跑出合理结果再往下一步拓展。如果小规模算例就解不动,一定是模型或代码逻辑错了,这时候停下来排查,远比硬撑着让求解器空转有效。
代码里我还习惯把约束按物理含义分组存进cell数组,比如Constraints{1}存设备约束,Constraints{2}存联络线约束,Constraints{3}存KKT条件。出问题时直接检查对应的那组约束,定位速度比一张大网式拼接快得多。
最后分享一个我个人调试KKT转化模型的心得:模型解出来之后,除了看目标函数值,顺手把互补松弛条件的残差也打出来。数值上到1e-6甚至1e-8是很正常的;但如果残差落在1e-2级别,八成是大M取小了,或某条约束的乘子符号写反了,先回头查乘子的正负,比从头看公式要快得多。这套方法帮我省下过好几个通宵,希望你也能用上。