上篇写预配置的时候,我把应急移动电源(MPS)的选址和容量配置讲完了。那一步解决的是灾前决策:在台风预警发布之后,把有限的三五台移动电源摆在哪些位置,才能让后续最坏情况下的恢复期望值最大。但真正动手做动态调度之后,我才意识到预配置只是这道题的前半截——故障发生后的十几个小时里,MPS去哪里、供多少功率、什么时候撤离转场,才是整个韧性链条里最烧脑的部分。
动态调度要回答的问题是具体的:已知哪些支路断了、哪些负荷窝在孤岛里、抢修队大概什么时候把哪条线路修好,站在当前时点上,每一台MPS是继续蹲在原来接入点放电,还是拔掉电缆转场去下一个失电区域?到达新接入点之后功率怎么定?什么时候动身才能避免在路上浪费掉最宝贵的“供电窗口”?这些决策交织在一起,构成了一个典型的时空耦合混合整数规划问题。
这篇就把MPS动态调度的完整实现拆开讲:数学模型怎么搭、Matlab里怎么用Yalmip+Gurobi落代码、在33节点配网算例上跑出来的韧性曲线长什么样,以及我复现过程中踩进去的四个坑。内容偏工程和代码,适合正在复现配电网韧性、移动储能应急供电这类方向,或者需要从论文公式走向可运行代码的人。这篇是下篇,默认你已经看过上篇的预配置模型,但如果你只对动态调度部分感兴趣,也可以直接往下看。
1. 动态调度到底在“调”什么:先从预配置的衔接说起
1.1 预配置与动态调度的分工
预配置和动态调度在时间尺度和决策性质上是两类问题。预配置发生在灾前,决策变量是“若干个候选站点,放哪几台MPS、每台多大容量”,它要处理的是未来故障场景的不确定性,常见做法是随机规划、鲁棒优化或者两者的混合。动态调度发生在灾后,决策窗口是故障发生到完全复电这段时间,决策变量变成“每台MPS每个时刻在哪、给谁供电、供多少、什么时候转场”。
这里有一个很多人忽略的点:预配置决定的是MPS的“初始可用状态”,而动态调度决定的是MPS的“全程可用状态”。一台MPS就算预配置位置再正确,如果调度模型僵化、不会跟着修复进程转移,它的价值也会打个大折扣。反过来,如果调度模型能动态调整,那么预配置即使稍微偏离最优,最终的韧性指标也不会差太多。这有点像备货和配送的关系:仓库建得好不好是一回事,配送路径和时机才决定了货能不能真正到达用户手里。
所以可以这样理解两个环节的分工:上篇解决“从哪个起点出发”,这篇解决“怎么走完整个旅程”。动态调度里的“动态”二字,指的不是时间维度本身,而是决策要对系统状态——故障修复进度、负荷变化、MPS余量——做出响应。修复进度一变,MPS的走位就要跟着变,这才是“动态”的核心。
1.2 三类核心决策变量:位置、功率、孤岛归属
MPS动态调度的决策变量可以归成三类。第一类是位置状态变量,用二进制变量 s(k,t,i) 表示第 k 台MPS在时段 t 是否接入候选节点 i。第二类是功率与能量变量,包括每台MPS在接入节点的输出功率 P(k,t,i)、总放电功率 Pdis(k,t)、剩余电量 E(k,t)。第三类是移动动作变量 mv(k,t,i,j),表示第 k 台MPS在时段 t 是否“正在从 i 向 j 转移”。这个变量是动态调度区别于普通储能调度的关键,也是模型复杂度上升的主要来源。
实际建模型中,位置和移动变量不是互相独立的。一个基本规则是:任何时刻一台MPS要么停在某个接入点供电,要么在路网某条弧上移动,二者只能选其一。为了实现这一规则,我引入了一个“位置唯一性”约束:对每个时段 t、每台 MPS k,所有接入状态和所有移动弧状态求和不超过1。这个约束是所有后续约束的地基——如果它不成立,模型很容易解出“一边在路上跑、一边还在给老节点供电”的荒谬方案。
孤岛归属变量 z(i,t) 也需要说明:它表示失电区域节点 i 在时段 t 是否被纳入MPS供电范围。在很多论文里,这个变量和配电网开关状态变量一起,决定了哪些负荷被“圈进”孤岛。复现时我做了简化:候选接入节点 i 与某个失电区域一一对应,接入即意味着该区域被恢复供电。对研究动态调度的主线来说,这个简化不会改变结论方向,但能显著降低变量规模,让模型在Matlab里跑得更快。
1.3 为什么不能把动态调度当机组组合做
第一次看这类问题的朋友容易产生一个疑问:MPS动态调度跟传统的机组组合(unit commitment)或者多时段经济调度不是一回事吗?都是“离散开停机/位置变量+连续功率变量+目标最大化”的组合。区别非常大。
机组组合假设发电单元固定在线,发出的电通过输电网全网消纳,只要网络不越限,功率想去哪就去哪。MPS不一样,它必须通过道路网络迁移到特定的接入节点,才能给那块区域供电。移动过程需要时间,期间不能供电还在耗电;接入之后也不是全网络消纳,而是只能给局部孤岛供电。所以MPS调度是“交通网络上的离散移动”和“配电网上的连续潮流”耦合在一起的问题,建模时必须显式引入时空网络。
另一个差别在于动态调度的目标更偏向“恢复关键负荷”,而不是“最小化发电成本”。配电网韧性场景下,负荷有不同的重要度权重,目标通常写成最大化加权恢复电量。权重差一个数量级,调度结果就会完全不同:现实中你会毫不犹豫地把最后一度电留给医院和通信基站,模型里的权重就是干这个用的。
2. 时空-能量耦合模型:我最终采用的MILP表达
2.1 目标函数:加权恢复电量与移动惩罚
我复现时采用的目标函数是最大化故障周期内的加权恢复用电量,并叠加一个极小的移动惩罚项,形式如下:
max Σ_t Σ_i w_i · P_rec(i,t) · Δt − γ · Σ_{k,t,i,j} mv(k,t,i,j)
其中 w_i 是负荷区域 i 的重要度权重,P_rec(i,t) 是 t 时段区域 i 的恢复功率,Δt 是时段长度。γ 我取 1e-3 量级,纯粹用于抑制数值上出现的“无意义空跑”——因为移动本身不会给目标函数带来直接收益,不加这个惩罚项有时求出来的路径会有微小的无意义抖动。
权重 w_i 建议作为一个可配置参数暴露出来。我在算例里把关键负荷区域(比如医院、指挥中心)设为1.0,普通商业负荷0.5,一般居民负荷0.1。这样模型在能量紧张时自然会先砍低权重负荷的恢复,而不是平均分配。如果去掉权重,改成总恢复电量最大,解出来往往是“一团和气地均匀恢复”,这在韧性场景下是不符合实际需求的。
2.2 位置与移动约束:时空网络上的MPS运动学
位置与移动约束是这套模型的骨架。设 τ(i,j) 为从候选节点 i 到 j 的旅行时段数,由道路距离除以平均行驶速度向上取整得到。约束包含三部分:
第一,位置唯一性:
Σ_i s(k,t,i) + Σ_{(i,j)} mv(k,t,i,j) ≤ 1,对任意 k,t 成立。
第二,移动与接入的衔接。MPS 只有处在节点 i 的接入状态,才能从 i 出发去 j;到达 j 之后必须占用 j 的接入状态,不能“悬空”。用代码表达就是:
% 从 i 出发前,必须处于 i 的接入状态 Constraints = [Constraints, mv(k,t,i,j) <= s(k,t,i)]; % 到达 j(t+tau_ij时段)后,必须处于 j 的接入状态 if t + tau(i,j) <= T Constraints = [Constraints, mv(k,t,i,j) <= s(k,t+tau(i,j),j)]; end第三,旅程覆盖互斥。如果 MPS 在 t 时刻从 i 出发前往 j,那么在 [t, t+τ-1] 整个移动窗口内它都不能接入任何节点。这个约束是很多人漏掉的,后面坑一会详细讲。
如果候选节点少、时段粒度粗,也可以把移动做进一步简化:假设“一个时段内可以在任意候选节点间转移”,此时 τ 全部等于1。这个简化在论文复现中很常见,但会乐观估计 MPS 的机动性。我最终版保留了真实旅行时间,用向上取整的时段整数量化,简单且物理可解释。
2.3 能量-功率耦合与负荷恢复约束
能量约束主要是一条SOC递推式:
E(k,t+1) = E(k,t) − Pdis(k,t)·Δt − E_move(i,j)·mv(k,t,i,j)
其中 E_move(i,j) 是移动一次消耗的能量,我按“移动基础功率 × 旅行时间 + 单位里程电耗 × 道路距离”计算。注意移动能耗要和 mv 变量相乘,只有真正发生了转移,这部分的能量才从SOC里扣掉,否则模型会凭空多出一份“白送”的移动能耗,导致结果过度乐观或者无解。
功率约束也比较直接:MPS的输出功率只有在其接入状态下才允许非零,且不能超过最大输出功率。这里需要警惕的就是 BigM 的取值,后面会单独讲。负荷恢复约束是:P_rec(i,t) 不能超过该区域 t 时段的负荷需求,也不能在没有MPS接入时非零。如果有MPS接入,则该区域的恢复功率等于各MPS在该节点注入功率之和(简化模型中忽略网损和电压约束)。
复现版本里我用的是“区域功率平衡+容量校核”的简化潮流,没有逐节点迭代Distflow。原因很简单:动态调度重点在于MPS的时空决策,潮流细节虽然影响最终电压,但它会让模型变成更复杂的非线性/高阶约束,求解难度指数上升。论文里如果严格做了Distflow线性化,效果会更好;如果你想扩展,可以把Distflow加入约束组,模型的骨架不需要改。
3. 从公式到Matlab:Yalmip建模的关键实现
3.1 工程化代码组织
Matlab 上做这种MILP建模,最优选择就是 Yalmip 统一建模 + Gurobi/CPLEX 求解器。Yalmip 的语法直观,尤其擅长处理二进制变量和稀疏约束。我建议不要把代码全部塞在一个 main 里,而是按下面这个结构组织,后续调试会轻松很多:
main_mps_dispatch.m ├── data │ ├── case33bw.m % 33节点配电系统数据 │ └── fault_scenario.m % 故障支路、修复时序、负荷权重 ├── lib │ ├── build_spacetime.m % 生成候选节点、距离矩阵、旅行时间 │ ├── build_yalmip_model.m % 整个MILP模型 │ ├── solve_and_postprocess.m │ └── rolling_horizon.m % 滚动时域闭环调度 └── plot ├── plot_trajectory.m % MPS轨迹/甘特图 └── plot_recovery_curve.m % 恢复率曲线这个结构的好处是每个文件职责单一:数据文件单独放,是方便你换算例系统(比如换成IEEE123节点)时不用动模型代码;模型函数单独放,是方便你在不换数据的情况下调试算法(比如把开环换成滚动时域)。
3.2 变量定义与约束写入
在 Yalmip 里定义变量时,我建议把维度一次性定义清楚,并用有意义的缩写注释。以下是我代码里的核心变量定义:
s = binvar(nMPS, T, nCand, 'full'); % s(k,t,i): k在第t时段是否接入节点i mv = binvar(nMPS, T, nCand, nCand, 'full'); % mv(k,t,i,j): 转移动作/移动弧 P = sdpvar(nMPS, T, nCand, 'full'); % P(k,t,i): 接入节点i时的注入功率 Pdis= sdpvar(nMPS, T, 'full'); % 总放电功率 E = sdpvar(nMPS, T+1, 'full'); % SOC曲线 P_rec = sdpvar(nCand, T, 'full'); % 各区域恢复功率约束写入尽量用向量化的方式。比如位置唯一性,如果一层层循环逐个约束写,Yalmip 解析很慢;直接写成:
Constraints = [Constraints, sum(s,3) + sum(sum(mv,3),4) <= ones(nMPS,T)];一行搞定。这里 sum(s,3) 把候选节点维求和得到 nMPS×T 的矩阵,sum(sum(mv,3),4) 把所有移动弧求和得到 nMPS×T,和矩阵每个元素对应一台MPS一个时段的全部状态。这种写法在变量规模增大后优势非常明显。
目标函数和求解部分也是几行的事情:
obj = -sum(w .* P_rec, 'all') * dt + 1e-3 * sum(mv, 'all'); ops = sdpsettings('solver', 'gurobi', 'verbose', 2, 'gurobi.MIPGap', 0.01); sol = optimize(Constraints, obj, ops);如果没装 Gurobi,可以临时把 solver 换成 intlinprog 之类的内置求解器,但求解速度会明显下降,建议正式跑数还是用商用求解器。
3.3 求解配置与滚动时域扩展
求解完成后,先检查 sol.problem 是否为0,再通过 value() 取回变量:
if sol.problem == 0 s_val = value(s); mv_val = value(mv); P_val = value(P); P_rec_val = value(P_rec); E_val = value(E); else [primalres, ~, ~, ~] = check(Constraints); % 查看最大约束违反量,定位无解原因 end滚动时域调度的扩展在工程上非常有意义。思路是:并不一次性解完24小时,而是每个决策周期只看未来 H 个时段的窗口,求解后只执行第一个时段的决策,然后滚动推进。Matlab 里用 for 循环嵌套即可,核心是把当前时段的初始状态(位置、SOC)硬约束进模型,同时把窗口外的变量去掉或固定。我在 lib/rolling_horizon.m 里实现了这个逻辑,后面算例会给出 H 取不同值时的性能差异。
这个扩展之所以重要,是因为现实中故障修复时间高度不确定。开环优化假设抢修队第6小时修好支路,实际上可能第4小时就通了,也可能拖到第9小时。一旦实际情况偏离预测,开环方案就局部失灵;滚动时域每次根据最新状态重新优化,才能把不确定性“滚掉”。
4. 33节点算例实测:韧性曲线是怎么被拉回来的
4.1 算例参数与故障场景
我用的是 IEEE 33 节点配电系统,基准电压12.66kV,系统参数直接取标准数据。MPS 设置为3台,每台容量300kWh,最大输出功率100kW,平均移动速度20km/h,移动基础功率15kW(移动每小时耗能15kWh),单位里程电耗0.6kWh/km。时段分辨率取1h,仿真周期24h。
故障场景设定为主馈线发生三处断线:支路5-6、9-10、14-15 同时故障,形成三个失电区域。修复时序设置为14-15支路在第6小时修复、9-10支路在第12小时修复、5-6支路在第18小时修复。这就意味着,区域C在第6小时后恢复主网供电,区域B在第12小时后恢复,区域A最晚恢复。
候选接入节点我取了三个区域的主干节点:节点6对应区域A,节点10对应区域B,节点14对应区域C。权重分别设为0.5、1.0、0.3,其中区域B是关键负荷区。节点之间的道路距离我按 5~12km 随机生成并固定下来,旅行时间用距离除以速度向上取整得到。
下表是不同策略的对比结果,数据是我在这个简化算例下跑出来的示例结果,趋势比绝对数值更有参考价值。
| 方案 | 加权恢复率 | 关键负荷恢复率 | 累计恢复能量(MWh) |
|---|---|---|---|
| 无MPS | 0% | 0% | 0 |
| 静态部署(预配置后MPS不动) | 61.3% | 77.8% | 28.4 |
| 动态调度(开环全局优化) | 82.4% | 94.1% | 38.7 |
| 动态调度(滚动时域 H=6) | 78.9% | 90.5% | 36.2 |
4.2 结果曲线怎么读:轨迹图、恢复率、SOC
跑完之后最直观的输出是MPS轨迹图,我习惯画成类似甘特图的形式:横轴是时间,纵轴是三台MPS,每一段用不同颜色表示位于哪个接入节点,灰色段表示在移动。从图上能一眼看出调度方案的“走位”是否合理。
我跑出来的开环结果里,三台MPS的走位很有代表性:MPS1 先蹲在区域B支援关键负荷,等到区域B在12小时恢复主网后,它没有继续待着,而是转场去了还没恢复的区域A;MPS2 从区域C出发,在区域C修复后立刻转移到区域A接力;MPS3 则一直在区域A通过节点6供电。三台车形成了交错接力的节奏,而不是各自蹲守原区域。这就是“动态”二字的直接体现。
对比静态部署方案,动态调度多出来的收益主要来自两处:一是区域修复后及时撤出MPS,把能量留给仍然失电的区域;二是通过提前预估旅行时间,避免出现“MPS到新区域时没电了”的尴尬局面。在结果图里,你可以看到静态部署区域B的关键负荷在12小时后依然有功率供给,但此时其实已经不需要了,而区域A的负荷却因为能量不足只能降级恢复。
4.3 数量、容量、窗口长度的敏感性
做 SCI 复现时,光跑一组结果是不够的,审稿人肯定会追问参数的敏感性。我快速跑了几组:
敏感1:MPS数量。1台MPS时加权恢复率只有大约 47%,因为一台车只能蹲一个区域,而且移动到另一个区域时原区域就彻底没人管;2台时到 68%;3台到 82%。数量从2到3收益仍然不小,但这个收益受候选节点数限制——当每个区域都有车占住后,再加车就只能挤在同一个节点,收益骤降。
敏感2:MPS容量。把单台容量从300kWh提到500kWh,恢复率提升并不明显,大约 84%。原因是容量在算例里已经超过了单区域剩余时段的负荷需求,瓶颈变成了MPS数量和移动时间。反过来容量降到100kWh,恢复率掉到 55% 左右,因为车还没到下一个区域就接近没电,机动性被能量硬卡住。
敏感3:滚动时域窗口 H。H=2时恢复率掉到 70% 左右,而且轨迹图显示MPS频繁转场,典型的“只见树木不见森林”;H=6恢复到79%,H=8以上基本接近开环水平。实际工程中,H取“最长旅行时间+2”以上比较稳妥,既能顾及中期态势,又不至于让每轮优化太重。
5. 复现中我踩过的四个坑:从无解到错误解
5.1 坑一:移动与供电互斥没锁住
我第一版模型跑出来的解非常离谱:某台MPS明明在移动弧上已经“出发”了,同一时段它原来的接入节点还在往外输出功率。问题出在互斥约束只约束了出发时刻,没有约束移动过程中的所有时段。
具体说,我一开始写的出发衔接是: mv(k,t,i,j) <= s(k,t,i)
这只能保证“出发那一刻它确实在 i”,但移动过程跨多个时段,中间这些时段 s 变量是自由的,模型当然会搞出“人在路上,电还在供”的方案。
解决方式是在移动窗口内的每个时段都加互斥约束:从 t 出发、旅行时间为 τ,则对 t' ∈ [t, t+τ-1],都有 Σ_i s(k,t',i) ≤ 1 − mv(k,t,i,j)。写成向量化形式也不难。这个坑给的经验是:涉及移动/转移类模型中,状态互斥必须作用在整个转移区间,而不是只作用在端点。
5.2 坑二:移动能耗没进SOC递推
第二个坑更隐蔽。第一版我为了快速验证模型,在SOC递推里只写了放电项,没写移动能耗项。结果三台MPS从区域A转到区域C再到区域B,行程几十公里,SOC曲线纹丝不动——这等于MPS是“白嫖”的移动,不用消耗能量。看起来目标函数值很好,但物理上完全站不住脚。
把移动能耗项加进递推之后,模型反而先报了一波无解。一开始我以为建模错了,检查后才发现是好几条转场路线的距离太远、移动能耗加起来太大,而MPS的初始SOC又相对有限,导致某些看似“很灵活”的走位根本不可行。这正是真实系统里的硬约束。
这个经历说明一个问题:模型越贴近物理,解的数量越少,但剩下的解才真正可信。复现论文时如果发现结果好得过分,优先检查是不是有物理约束被忽略了。
5.3 坑三:BigM取值太松
复现时有一版求解非常慢,Gurobi跑了快一小时,MIPgap才降到20%。最后定位到原因是BigM系数写得太大:写“P ≤ BigM * s”,我当时顺手填了1e5。线性规划松弛之后,MPS在不接入节点时也能把P撑到很大,整个下界松得没法看,分支定界效率极低。
正确的做法是把BigM取到物理上最紧的上界,比如最大输出功率100kW,或者再乘个1.1的安全系数。改成这种紧BigM之后,同一模型几分钟就收到1%的MIPgap。这条经验对所有MILP建模都适用:BigM不是随便取个“够大”的数就行,取多大会直接影响求解器的收敛曲线。
5.4 坑四:时序索引差一个时段
还有一个非常容易犯的低级错误:时序索引偏移。旅行时间为2个时段,从 t 出发,到达时刻应该是 t+2,但我在一条约束里写成了 t+1,导致MPS到达后“多赚”了一个时段的供电时间。这种错误在最终结果里不太容易被肉眼看出来,但如果你把轨迹图和区域复电时刻对齐,会发现MPS在某些区域的接入时间比理论上多了整整1小时。
定位这类问题的办法是写一个小规模人工可验证的用例:一台MPS、两个候选节点、固定旅行时间,手动推一遍期望的轨迹,再跑模型对比。把这套冒烟测试放在代码目录里,以后改模型时跑一遍就能快速排除低级错误。
6. 给复现者的几点建议
如果你想把这个模型复现到自己数据集上,我的建议是:先跑通小规模冒烟测试,再上大规模算例。我调试时先用一个6节点简化系统、12个时段、1台MPS,模型只有几十个变量,几秒出解。确认位置唯一性、移动衔接、SOC递推、目标函数四块逻辑都对之后,再切换到33节点系统、3台MPS、24时段,这时候绝大多数报错都是规模问题而不是逻辑问题。
模型参数建议全部做成可配置项——权重、惩罚系数、BigM、旅行时间、修复时序,都要能从配置文件读进来。一区论文里的参数设置往往直接影响结论,复现时不要硬编码,否则后面做敏感性分析时你会想砸电脑。
如果后续想从“复现”走向“工程落地”,滚动时域闭环是必须做的一步。开环优化适合离线的性能上限评估,现场调度面对的是不断变化的修复进度和负荷曲线,只有滚动修正才能让MPS的计划真正跟得上现实。代码上可以把滚动窗口 H 设为可配置,跑一遍 H 的敏感性,你就能直观看到“短视”和“远见”之间的差距。
我在复现这套动态调度时最深的体会是:MPS的机动性是珍贵的,但也是有代价的——移动期间不能供电,还要消耗自身能量。一个调度方案真正的难点,不是“去哪”,而是“什么时候舍得放弃眼前能供的负荷,转而去救远处更重要的负荷”。把这个权衡完完整整建进模型、让优化器替你做取舍,韧性曲线自然会被拉起来。