☰
配电网韧性提升:移动储能预布局与两阶段鲁棒优化详解
2026/10/21 19:01:35 网站建设 项目流程

做配电网韧性方向的研究,移动储能(MES)几乎是绕不开的一个点。极端天气来了以后电网大概率会被撕开好几个口子,固定储能只能守在原地,而移动储能可以灾前预布局、灾后动态调度,这种时间和空间上的灵活性正好打在韧性的需求上。最近我在IEEE33节点系统上把“移动储能预布局+动态调度”的Matlab实现完整跑通了一遍,从模型、求解到结果可视化都没有踩不过去的地方,这篇文章把整个建模思路、C&CG求解细节、代码结构和复现过程中容易踩的坑全部拆开讲清楚。正在做配电网韧性提升、移动储能调度,或者想快速跑通一个IEEE33节点算例做两阶段鲁棒优化的同学,这篇文章应该能省你不少时间。

1. 移动储能为什么能成为配电网韧性的关键变量

1.1 可靠性与韧性不是一回事

我见过不少刚开始接触这个方向的同学,把“韧性提升”和传统可靠性优化混在一起来理解。配电网可靠性关注的是常规故障,比如设备老化、雷电引起的短时故障,停电时间通常以小时计,靠网架结构优化、继电保护配合和固定设备冗余就能解决。韧性不一样,它面对的是台风、冰灾、暴雨这类低概率、高影响事件,一次故障可能让一大片区域停电数天,问题的关键已经从“尽量不停电”变成了“停电之后能多快恢复”。

IEEE33节点这种典型的放射状配网,日常可靠性其实还行,但一旦主干线路被打断,下游整片负荷都会失去主网供电。这时候常规的固定储能只能站在原本的位置干等,而移动储能车可以主动切入孤岛,临时形成局部供电。补位速度越快,停电损失就越小,这就是移动储能在韧性框架里被单独拎出来研究的原因。

1.2 移动储能的三重角色:预部署、应急接入、动态转移

移动储能在韧性场景下至少干了三件事。

第一件,灾前预部署。预警窗口只有几个小时,车辆从车场出发,提前开到预测的失电区域边缘或者负荷中心待命,保证灾后道路拥堵和损毁都不影响它到达关键位置。第二件,应急接入。故障刚发生、抢修队还在路上的时候,移动储能可以先接入一个节点,作为临时电源给医院、泵站这类重要负荷供电,把停电影响压到最小。第三件,动态转移。伴随线路修复过程,它还能不断更换接入点,比如下游快修好了就往上挪一步,去补另一个失电的局部区域。

固定储能像家里的饮水机,放哪就在哪用;移动储能更像送水车,哪里缺水就往哪里开。韧性恢复恰恰需要“送水车”,而不是“饮水机”。

1.3 预布局和动态调度本质上是一个两步决策问题

把上面三个角色拆开看,预布局和动态调度其实是两个决策层。灾害预警阶段,我们要决定每台储能车停到哪个候选节点,这个决策在故障发生前就必须做,属于“现在就要定”的决策;故障发生后,储能车在哪个节点出力、出多少、充放怎么安排,取决于实际故障场景,属于“看到场景再决定”的决策。

这种结构在优化里非常典型:第一阶段变量先确定,第二阶段面对不确定性再优化。很多论文把它写成两阶段鲁棒优化,目标是在最坏故障场景下仍然让加权失负荷尽量小。我复现的时候用的就是这个框架,第一步先定预布局,第二步再把每个时刻的调度计划算出来,这两步不是分离的,而是靠迭代求解耦合在一起的。

2. 两阶段模型怎么建模:预布局与动态调度的数学骨架

2.1 目标函数:最坏故障场景下的加权失负荷最小化

目标函数是我最先定下来的。我采用的形式是:第一阶段行驶部署成本加上第二阶段在最坏故障场景下的加权失负荷。代码里可以直接写成:

min ∑_{s∈S} ∑_{n∈N} c_move(s,n)·x(s,n) + max_{u∈U} min_{y} ∑_{t∈T} ∑_{n∈N} w_n·(1 − r_{n,t})·P_L(n)

这里x是预布局的0-1变量,u是故障场景,r是负荷恢复比例,P_L是节点额定负荷,w_n是负荷权重。为什么把失负荷当主项?因为极端事件下,少停一个小时电带来的社会效益远大于省下的那点车辆里程费用。

我在算例里把负荷分成三级:医院、应急指挥中心、供水泵站这类重要负荷权重给3;商业和工业负荷给2;普通居民给1。权重直接影响预布局选址——移动储能会更愿意停在靠近重要负荷的位置,而不是一味追求离故障点最近。

2.2 预布局约束:车的位置、数量、时间窗与候选节点

预布局的硬约束不多,但每条都容易写错。

首先是位置约束,每台车最终只能停在一个节点:

sum_{n∈N_cand} x(s,n) = 1, ∀s

然后是车位约束,每个候选节点能容纳的储能车数量有限:

sum_{s} x(s,n) ≤ cap_n, ∀n

还要加上时间窗约束。如果从车库到节点n的行驶时间是T_move(s,n),预警窗口是T_pre,那么T_move(s,n)·x(s,n) ≤ T_pre必须成立,否则预布局根本来不及。

IEEE33节点里我预设了3台移动储能,单台额定功率250kW、容量500kWh,车速40km/h,这些参数放进去之后,能从车场在2小时预警窗口内到达的候选点就筛掉了一半。候选节点我选的是节点6、12、18、22、25、33这类下游负荷较重或者分支末端的关键位置,没把33个节点全纳入——后面会专门讲为什么不能贪多。

2.3 动态调度约束:线性化潮流与移动储能时序约束

第二阶段的动态调度约束是整个模型的骨架。配电网有功和无功潮流的DistFlow形式可以写成:

P_{ij,t} = ∑_{k∈δ(j)} P_{jk,t} + P_L(j,t) − P_MES(j,t) Q_{ij,t} = ∑_{k∈δ(j)} Q_{jk,t} + Q_L(j,t) − Q_MES(j,t) V_{j,t}² = V_{i,t}² − 2(R_{ij}P_{ij,t} + X_{ij}Q_{ij,t})

这里的V²是非线性项,直接放进模型会让子问题变成非线性规划,对偶也不方便。处理办法很简单:把V²当作一个新变量W_j,t来做线性化。IEEE33节点基准电压12.66kV,忽略网损之后电压精度稍微差一点,但作为两阶段鲁棒优化里的约束校验完全够用。

移动储能的时序约束同样用线性表达:

E_{s,t+1} = E_{s,t} + η_ch·P_ch(s,t)·Δt − P_dis(s,t)/η_dis·Δt 0 ≤ E_{s,t} ≤ E_cap(s) 0 ≤ P_ch(s,t), P_dis(s,t) ≤ P_rated(s) P_ch(s,t) + P_dis(s,t) ≤ 1

最后一条是充放电互斥约束,工程上防止电池同时充放。我在实际代码里没有用严格的互斥约束,而是把P_ch和P_dis同时设为非负但目标函数里放一个很小的运行成本项,让求解器自动避免同时充放,这样能少引入一票0-1变量,求解速度会快一些。

2.4 故障场景集:从线路到配网孤岛的输入条件

故障场景我用线路故障集来表示。IEEE33节点一共32条支路,极端事件下故障线路条数通常在某个范围内波动,不确定集合U可以限定故障线路数量上下界,比如至少3条、至多8条,这样既贴近现实又不会让场景空间大到爆炸。

举个例子,如果线路5-6故障永久切除,从节点6往后的整片负荷,包括6-18这条主干以及26-33分支带出来的所有支路,都会脱离主网供电,形成一个大孤岛。移动储能如果预布局在节点6附近,故障后几分钟就能接入,先恢复一部分重要负荷;如果车还在远处的车场,等到道路疏通再开进去,关键负荷可能已经停电数小时。

这个例子是我选择候选节点时最重要的依据,预布局的选址本质上是“提前预测哪些节点会在故障后成为孤岛入口”。

3. C&CG求解框架:为什么这样选以及主问题/子问题怎么拆

3.1 为什么不用场景枚举

第一次做这个模型的时候,很多人第一反应是枚举所有故障场景,毕竟IEEE33只有32条支路。但你只要算一下2的32次方就能意识到这条路走不通。即使只考虑5条线路同时故障的组合,场景数目也在十万级别,每个场景解一次调度问题,复现一次要跑好几个小时。

两阶段鲁棒优化不是要遍历所有场景,而是通过迭代让算法自己找到那个“最坏场景”,然后用割约束把最坏场景对应的调度方案不断塞回主问题,逼近问题下界。这也是C&CG在这个问题里成为主流方法的原因——相比Benders分解,C&CG每轮迭代加入的是完整场景变量和约束,包含更强的信息,在IEEE33这类规模不大的配网算例上通常十几轮就能收敛到很紧的gap。

3.2 主问题与子问题分工

C&CG的循环框架我直接写在主程序里:

初始化:场景集S = {一个基准故障场景};UB=∞;LB=−∞;iter=0 while (UB−LB)/UB > ε 1. 解主问题MP,得到预布局方案x*和LB 2. 固定x*,解子问题SP,得到最坏场景u*和目标值OBJ_SP 3. UB = min(UB, OBJ_SP) 4. 把u*对应的场景变量和调度约束加入MP 5. iter = iter + 1 end

主问题在每一轮包含已经生成的所有场景。每加入一个场景,主问题里就多一整组储能调度变量,所以主问题规模是随迭代次数逐渐变大的。子问题则始终保持单场景规模,固定预布局方案后去“对抗”故障场景。

这里有个很容易犯的错误:把子问题的目标值直接当作上界,但没有考虑主问题里预布局成本。我实现时SP的目标是max-min形式的加权失负荷,把预布局成本留在MP里,这样UB和LB才有可比性。

3.3 子问题单层化的核心操作与对偶前提

子问题的结构是max_{u∈U} min_y f,这是一个双层问题,求解器没法直接处理。把内层min_y对偶成max,就能把两个max合并成单个max问题。

这一步成立的前提是:固定x之后,内层调度问题必须是线性规划,满足强对偶条件。所以我在建模阶段刻意把潮流约束线性化、储能模型线性化,目标函数里不出现任何二次项或0-1调度变量,就是为了让子问题能够干净地单层化。

实际代码里我为了省事没有手动写对偶表达式,而是直接对子问题内层调用YALMIP的dual或者用求解器自带的LD,本质上是一样的。手动写对偶的好处是快,坏处是索引容易错;自动对偶的好处是安全,缺点是会多花一点建模时间。复现论文的时候我建议先自动对偶跑通,再考虑要不要手工优化。

4. Matlab代码落地的完整路径与核心结构

4.1 文件结构

我把一套可运行代码按模块拆开,每个文件只干一件事:

main_CnCG.m 主迭代入口,C&CG循环 case_ieee33.m IEEE33节点系统数据与拓扑矩阵 build_MP.m 主问题建模 solve_SP.m 子问题建模与求解 update_cuts.m 割约束/场景集更新 move_storage_data.m 移动储能参数与车场信息 plot_results.m 结果可视化,生成韧性曲线

这种分层同样适合改到其他配网算例,你只需要替换case_ieee33.m里的节点支路数据和负荷数据,主程序几乎不用动。

4.2 预布局变量和主问题建模代码

主问题用YALMIP建模,预布局决策变量是二进制的x,调度功率是连续的Pdis/Pch。核心代码片段大致是:

x = binvar(n_mes, n_cand, 'full'); Pdis = sdpvar(n_mes, n_node, n_T, 'full'); Pch = sdpvar(n_mes, n_node, n_T, 'full'); E = sdpvar(n_mes, n_node, n_T, 'full'); Constraints = []; for s = 1:n_mes Constraints = [Constraints, sum(x(s,:)) == 1]; % 每车一个位置 Constraints = [Constraints, E(s,:,1) == E_init]; % 满充出发 end for n = 1:n_node Constraints = [Constraints, sum(sum(x,1),2) <= cap_node(n)]; end Objective = pre_cost + sum(sum(sum(w .* (1 - r) .* P_L))); ops = sdpsettings('solver', 'gurobi', 'verbose', 2); optimize(Constraints, Objective, ops);

注意我把每个场景的调度变量都堆在同一个优化问题里,YALMIP会对每个变量名生成矩阵索引,你没定义到的节点/时段会自动补0。这里最怕的是变量索引越界和w与P_L维度不匹配,建议用size检查。

4.3 子问题与割约束生成

子问题解法上,固定x后,保持u为0-1故障线路变量,内层调度用线性DP模型。我复现时采用的流程是:

function [obj_sp, u_new, flag_conv] = solve_SP(x_fixed, data) u = binvar(n_branch, 1); % 故障线路指示 Constraints = [Constraints, sum(u) <= u_max, sum(u) >= u_min]; ... optimize(Constraints, -inner_obj, ops); % max-min 合并后的单层问题 obj_sp = value(inner_obj); u_new = value(u); if obj_sp >= UB - 1e-4 flag_conv = 1; % 已经不能再恶化 else flag_conv = 0; end end

子问题里的u每一个0-1变量代表一条支路是否故障。为了不让u和调度变量相乘出现双线性项,所有“故障后节点不能供电”的限制都用方向性约束表达。如果你发现代码里出现u.*Pdis这种写法,那就已经踩进双线性坑了。

4.4 求解器配置与结果输出

使用Gurobi或Cplex时,MIPGap要单独设置。我跑IEEE33算例时主问题从第1轮的解算时间8秒左右开始,迭代到第15轮后单轮时间会涨到40秒。建议gap设0.1%,时间限制设在3600秒,避免个别大场景把整晚时间吃掉。

输出阶段我会保存三张图:负荷恢复率曲线、储能的SOC变化曲线、节点电压分布。复现论文时最关键的验证图是韧性曲线,横轴是时间,纵轴是恢复负荷比例,预布局方案和无预布局方案的曲线差异足够直观,也是审稿人最认的一张图。

5. 复现过程中最容易翻车的六个细节

5.1 潮流线性化的精度校验

把V²当变量处理之后,IEEE33的电压结果和标幺值潮流对比会有偏差,特别在末端节点18和33。我的做法是先跑一次MATPOWER对照,把线性化误差控制在0.5%以内。如果你的模型要考虑电压越限约束,建议用二阶锥精确化,代价是子问题从LP变成SOCP,对偶处理要相应调整,C&CG的合并逻辑要重写,复杂度会上一档。

5.2 候选节点太多导致主问题爆炸

把33个节点全部作为候选点,预布局变量就是3×33个0-1变量。看着不多,但主问题每加一个场景就多3×33×24个连续变量,迭代15轮之后规模很可观。我实际只用了6个候选节点,3台车的0-1变量只有18个,主问题规模小了一个数量级,求解速度反而更快,而且结果几乎没变。

原理在于移动储能车数量少,候选点多出来的位置根本不会入选,筛选候选节点并不会明显牺牲最优性。这是我在复现中很实用的一步简化。

5.3 双线性项怎么避开

有人把“储能接入点不能是故障节点”写成了z(s,n,t) <= u(n),表面看这是线性约束,没啥问题;但如果你在目标函数里写“失负荷量等于未故障节点的负荷乘以恢复比例”,比如cell_load = (1-u(n)) * P_L(n,t),这里u是0-1变量,P_L是常数,依然是线性,因为P_L不是变量。

真正的双线性项出现在两个决策变量相乘,比如储能出力和故障状态同时作为变量进入目标函数或约束。避开方法很简单:故障状态的影响只通过常数和不等式约束往下传,不让它出现在含调度变量的乘积里。一旦YALMIP提示bilinear或求解器进入非线性路径,基本就是这个位置写错了。

5.4 C&CG的gap与收敛判断

C&CG循环的gap用(UB-LB)/UB计算,注意UB和LB都是带预布局成本的完整目标值。我复现时发现前5轮gap下降最快,后面几轮经常只降0.1%以内。

判断收敛时不要用相等比较,用1e-4容差;如果连续两轮返回相同的u*,说明场景已经稳定,可以直接终止,这时候再跑下去只会无限增加主问题规模。另外,主问题的LB可能因为预布局成本的存在出现负值,gap公式仍成立,但要在代码里加一个max(0, LB)的保护,防止除零。

5.5 时间轴索引对齐

这个坑不起眼但很容易阴人。定义SOC变量E(s,n,t),如果t=1是第一个时段,那么初始SOC应该写E(:,:,1)=E_init,而不是E(:,:,0),Matlab也不存在0索引。如果你习惯从0时刻开始推演,代码里就必须把初值放在t=1,约束E(s,n,2) = E(s,n,1) + ...,否则第一段调度会凭空多出一段能量。

我在代码里用了统一的for t=1:n_T,注释里写明第t个时段从t时刻到t+1时刻,初值E(:,:,1)对应0时刻状态,这样索引就不会乱。

5.6 求解器环境与版本兼容

YALMIP、Gurobi、Matlab三者的版本兼容性经常出问题。最典型的报错是“Unable to find a solver”或者“No suitable solver for ...”。遇到这种情况不要急着怀疑模型,先跑一下yalmiptest,确认YALMIP能否正常识别Gurobi。我当前的运行环境是Matlab R2023a搭配YALMIP 2023版和Gurobi 10,还留意到新版Gurobi对旧Matlab的API支持有差异,如果模型求解报一堆solver相关的warning,优先升级YALMIP到release版。环境和版本问题属于“功亏一篑”的典型坑,代码和模型都对了,最后卡在求解器上,非常不值得。

最后再分享一个我个人的复现顺序:先固定一个基准故障场景,把动态调度模型跑通,验证SOC曲线和恢复率都符合直觉;再开放故障场景,把单场景的调度模型变成子问题;最后才加预布局二进制变量和C&CG迭代。每一层都有明确的验证目标,中途出问题很快能定位到是调度模型的问题、场景建模的问题还是迭代逻辑的问题。移动储能这个方向现在很热,IEEE33又是入门标准算例,把这套“预布局+动态调度”的完整链路彻底跑明白,后面换配网拓扑、加综合负荷模型、考虑多类移动资源,都只是在这个骨架上做增量。如果卡在某个具体报错上,欢迎带着代码来交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询