简介:面向配电网韧性提升的应急移动电源(MPS)动态调度MATLAB复现资源,以Lei等2019年IEEE Trans. Smart Grid论文第二阶段为蓝本,利用YALMIP工具箱完成混合整数规划建模与求解。资源聚焦事件发生后MPS动态派遣与配电网运行协同,处理不同时间尺度及道路-电力网络耦合难题,适合电力科研人员、研究生用于顶刊算法复现、对比与拓展研究。压缩包共5个文件,包括3个.m源文件(主程序、结果展示脚本与IEEE33节点系统数据)、1个.p受保护辅助代码和1个.vsdx拓扑示意文件,整体仅49KB,结构紧凑,便于直接定位模型与数据;目前已有322人学习/下载。借助该资源可直接运行第二阶段动态调度算例,观察MPS路由与调度策略对系统恢复力的提升效果。该套代码还可作为论文实验、课程设计或韧性评估课题的基础工具。
1. 配电网韧性提升为什么离不开“预配置 + MPS动态调度”这一个组合
做配电网韧性课题时,我常被问到一个问题:应急移动电源(Mobile Power Source,MPS)买了不少,灾前也提前布防了,为什么实际抢修时负荷恢复还是慢?答案往往不在设备数量上,而在动态调度策略上。同一批MPS,灾后先恢复哪个负荷、走哪条路、什么时候转移,决策不同,恢复的负荷电量能差出30%以上。这个标题里真正值钱的不是“预配置”那半篇,而是“下”这半篇:灾中MPS怎么动态调度。预配置解决的是“电源放在哪”,动态调度解决的是“电源在什么时候、以什么路径、给谁供电”。适合配电自动化、应急指挥和韧性评估方向的工程师读,也适合刚接触两阶段优化模型的研究生照着搭第一版程序。
2. 从预配置到MPS动态调度:两阶段优化框架先立住
2.1 为什么预配置不是“拍脑袋放几台车”
很多单位的应急预配置是凭经验做的:往负荷重、历史故障多的变电站门口放一台发电车就完事。但在配电网韧性评估的框架里,预配置是一个典型的“第一阶段决策”,它必须在故障场景不确定的情况下先落子,等故障真的发生了,再通过“第二阶段决策”也就是动态调度去弥补。两阶段之间的核心矛盾是:预配置做早了,信息不全;动态调度做晚了,电源来不及到位。
我一般会把预配置建模成“场景随机优化”里的第一阶段变量。目标函数里除了预配置成本,还要带上所有故障场景下动态调度的期望代价。这样预配置点选在哪,不是看哪个变电站负荷大,而是看“在所有可能故障场景下,MPS从预配置点出发能不能在关键时间窗内覆盖到关键负荷”。这是一个典型的选址-路径耦合问题,先想清楚这个框架,后面读任何一篇顶刊复现代码都不会迷路。
2.2 灾前到灾中:两阶段优化框架的分工与衔接
两阶段优化框架里,第一阶段是灾前的MPS预配置决策,第二阶段是灾后的MPS动态调度决策。两者的衔接点在于:第一阶段输出的预配置方案,直接作为第二阶段的“初始位置”输入;而第二阶段的调度成本,又反过来参与第一阶段的目标函数评估。常见做法是用样本均值近似(Sample Average Approximation)把随机场景离散成若干个确定性场景,每个场景跑一次动态调度,再把期望值求出来。
第二阶段动态调度里,决策变量通常包括:MPS在哪个时刻从当前位置出发、沿哪条道路移动、在哪个节点接入配电网、接入后以什么功率放电、什么时候断开并转移到下一个节点。这些决策要满足的约束包括:交通网络的时间约束、MPS容量约束、配电网潮流约束、负荷恢复的时序约束。动态调度不是把MPS当成“移动的充电宝”用,而是要把“移动轨迹”和“供电时序”联合优化,这两者常常是冲突的——走得越远,留给供电的时间越短。
2.3 定义电网与MPS参数:一份可落地的Python数据准备
动手写模型前,我会先把电网拓扑和MPS参数定义清楚。下面这份是简化但能跑通的初始数据准备代码,覆盖了IEEE 33节点系统的骨干结构和两台移动储能车的参数。
import numpy as np # 电网节点:0为变电站根节点,1~32为负荷节点 nodes = list(range(33)) # 负荷数据:{节点: (有功功率kW, 重要度权重)} # 权重越大表示越重要的负荷(医院、通信、供水优先恢复) load = { 2: (100.0, 1.0), 4: (120.0, 1.0), 7: (200.0, 10.0), 9: (60.0, 1.0), 14: (120.0, 5.0), 18: (90.0, 1.0), 24: (420.0, 10.0), 25: (420.0, 5.0), 30: (200.0, 1.0), } # MPS参数:{编号: (容量kWh, 最大放电功率kW, 移动速度km/h)} mps = { "M1": (500.0, 100.0, 30.0), "M2": (800.0, 150.0, 25.0), } # 预配置点:灾前MPS所在的候选节点(阶段一输出) pre_position = {"M1": 7, "M2": 14} # 交通网络弧段:(起点, 终点, 距离km) road_edges = [ (7, 9, 2.0), (7, 14, 1.5), (14, 18, 2.0), (14, 24, 2.5), (24, 25, 0.8), (25, 30, 3.0), ]这份代码里最值得注意的参数是负荷权重和MPS容量。负荷权重直接进入目标函数,权重设置不合理,调度结果就会偏离真实应急优先级;MPS容量决定它在几个节点之间能转移几次,容量太小的话,调度模型会自动让MPS“少跑多供”,这是对的行为,不用额外加惩罚项去约束它。road_edges 是后面动态调度里最短路径搜索和时序约束的数据基础。
提一句关于预配置点和动态调度衔接的做法:我把预配置输出直接当成 mps 字典的初始位置字段,然后在该约束里固定住。这样第二阶段模型天然接受第一阶段的结果,不用额外写接口转换,省掉一个常见的“数据对不上”的坑。
3. MPS动态调度模型:时空网络与路径-时序联合优化
3.1 把MPS移动过程写成“时空网络”
MPS动态调度和静态应急电源调度最大的区别在于“位置随时间变化”。如果不考虑时间维度,模型会给出一个荒谬的结果:MPS同时出现在两个节点上。所以第一步要做时空网络展开:把时间轴离散成若干个时隙,每个时隙内MPS要么在某节点供电,要么在某条路上移动。
我一般把时间步长设为1小时,故障恢复窗口设为72小时,这样每个MPS的决策维度就是72个时段。空间上,每个时段MPS可以选择“停留在当前节点”或“沿某条道路移动到相邻节点”。移动需要时间,移动时间内不能供电,这就是路径与时序耦合的本质。把这种“节点×时段”的二维决策空间建出来,MPS动态调度就从“调度问题”变成了“时空网络路径选择问题”。
构建时空网络时有个细节容易忽略:配电网的供电路径和移动电源的移动路径是两张不同的网。MPS在交通网上移动,在配电网节点上接入。两个网络通过“接入节点”这个集合关联。如果项目里只有配电网拓扑没有交通网数据,常见做法是把交通时间近似为节点间欧氏距离除以平均车速,再加一个1.2~1.5倍的绕行系数,误差可以接受。
3.2 动态调度的决策变量与约束
有了时空网络,MPS动态调度的决策变量可以分成这么几类:
- z[p][t][n]:MPS p 在时段 t 是否接入节点 n,0/1变量;
- w[p][t][a]:MPS p 在时段 t 是否在道路弧段 a 上移动,0/1变量;
- Pdis[p][t]:MPS p 在时段 t 的放电功率;
- s[p][t]:MPS p 在时段 t 结束时的剩余电量。
约束条件里最关键的是“位置唯一性”约束和“能量平衡”约束。位置唯一性要求每个MPS每个时段要么在一个节点供电,要么在一条弧段上移动,不能既供电又移动;能量平衡要求剩余电量等于上一时段剩余电量减去放电消耗,且任何时候不能超过容量上限、低于安全下限。第三个关键约束是“移动时间一致性”:如果MPS在t时段进入弧段a,而这条弧段要走2个时段,那它在t+1时段必须仍在该弧段上,不能中间瞬移。
import pulp as lp prob = lp.LpProblem("MPS_Dispatch", lp.LpMaximize) # 时空网络参数 T = 6 # 时段数,这里用6做小规模示例,实际取72 nodes = [2, 4, 7, 9, 14] # 候选接入节点 arcs = [(7, 9, 1), (7, 14, 1), (14, 18, 1)] # (起点, 终点, 通行时段数) MPS = ["M1", "M2"] init_pos = {"M1": 7, "M2": 14} # 决策变量 z = lp.LpVariable.dicts("z", (MPS, range(T), nodes), cat="Binary") w = lp.LpVariable.dicts("w", (MPS, range(T), arcs), cat="Binary") pdis = lp.LpVariable.dicts("pdis", (MPS, range(T)), lowBound=0) soc = lp.LpVariable.dicts("soc", (MPS, range(T + 1)), lowBound=0)变量定义好后,约束分三组加进去。位置唯一性约束保证MPS在每个时段只能做一件事;初始位置约束强制t=0时MPS在预配置节点;能量约束把soc、pdis和放电深度关联起来。完整模型代码不在这里展开,重点说一下pulp建模时容易踩的位置:LpVariable.dicts 用嵌套字典生成变量,索引顺序是 (MPS, T, nodes),后面引用约束时经常有人把索引顺序写反,导致“KeyError: (0, 'M1', 7)”这类错误。建议固定一种顺序并在注释里写明。
3.3 目标函数:恢复电量优先还是恢复时间优先
动态调度的目标函数直接决定MPS先救谁。常见目标有二:一是最大化恢复负荷总量(带重要度权重),二是最小化重要负荷的停电时间。两者并不等价——前者可能让MPS长时间趴在一个普通但电量大的负荷上,后者则倾向于“蜻蜓点水”式地每个重要节点都去停一下。
我在实际项目里的做法是混合目标:主目标是重要度加权恢复电量最大化,次目标是最晚恢复时间最小化。把次目标以极小权重放进目标函数里,或者做成epsilon约束。注意,次目标权重不能设太大,否则模型会牺牲恢复总量去“抢时间”,反而拖慢整体恢复进程。这类“目标打架”在动调度里很常见,建议先跑一版单目标,再对比混合目标看结果差异。
另外,目标函数里的负荷恢复量不是一个独立变量,它和z变量强相关:只有MPS接入负荷节点并放电,才有恢复量。这个逻辑可以在目标函数里直接用 z * load * 时段时长 表达,也可以额外引入 r[n][t] 表示恢复状态。显式引入恢复状态变量的好处是方便加“恢复后不可中断”的约束,这对医院、通信基站这类负荷是刚需。
4. 求解MPS动态调度:从MILP到分解算法的选择
4.1 先判断模型复杂度,再选求解器
MPS动态调度模型本质上是一个混合整数线性规划(MILP),变量里既有0/1赋值的z和w,又有连续功率变量Pdis和soc。小规模案例(节点数几十、时段数十几个、MPS两三台)直接用开源求解器就能跑;但到了真实配电网(上百个节点、72个时段、十几台MPS),整数变量规模会膨胀到几十万,单机直接求解会卡到怀疑人生。
判断模型能不能直接跑,我习惯看一个指标:0/1变量数量。经验上,单机MILP求解器能处理的0/1变量上限在10万量级,超过这个量级就该考虑分解算法或者启发式。以33节点系统、72时段、5台MPS为例,z变量是5×72×33≈11880个,w变量还要加上道路弧段数×时段数,总量大约3万以内,Gurobi或CBC都能在可接受时间内求解。
4.2 用Python + MILP求解器跑通最小动态调度模型
下面这个求解代码是我常用的一套最小可运行结构。它把第3章的模型加上目标函数、求解器设置和结果解析,跑通之后改参数就能往大系统上扩。
import pulp as lp # 目标:最大化重要度加权恢复电量 prob += lp.lpSum( z[p][t][n] * load[n][0] * load[n][1] * 1.0 # 功率 × 权重 × 时段小时 for p in MPS for t in range(T) for n in nodes if n in load ) # 位置唯一性约束:每个MPS每个时段只能接入一个节点 for p in MPS: for t in range(T): prob += lp.lpSum(z[p][t][n] for n in nodes) <= 1 # 初始位置约束:t=0时MPS必须在预配置节点 for p in MPS: prob += lp.lpSum( z[p][0][n] for n in nodes if n == init_pos[p] ) == 1 # 能量约束:soc(t+1) = soc(t) - 放电消耗 EFF = 0.95 # 放电效率 for p in MPS: prob += soc[p][0] == mps[p][0] # 初始容量 for t in range(T): prob += soc[p][t + 1] == soc[p][t] - pdis[p][t] / EFF # 求解 solver = lp.PULP_CBC_CMD(msg=True, timeLimit=600, gapRel=0.05) prob.solve(solver) # 输出调度方案:每个时段MPS接入的节点 for p in MPS: for t in range(T): for n in nodes: if lp.value(z[p][t][n]) == 1: print(f"{p} 在时段{t} 接入节点{n}")这里的PULP_CBC_CMD参数值得说明:timeLimit=600 是给求解器10分钟上限,gapRel=0.05 表示允许5%的最优性间隙。做工程验证时我一般先把gapRel放宽到0.1,确认模型没有语法错误和不可行约束后,再收紧到0.02~0.05。0/1变量多的时候,gapRel太小会让求解器把大量时间耗在证明最优性上,实际调度方案早就收敛了,这个“证明最优”的时间不值。
4.3 大系统怎么办:分解算法与滚动时域之间的选择
当模型大到单机MILP跑不动时,两条路线最常用,别一上来就上强化学习。第一是Benders分解:把动态调度问题按MPS分解成主问题和子问题,主问题决定接入节点和路径,子问题在给定接入方案下算潮流可行性和能量平衡,迭代加割平面。因为主问题是纯整数问题、子问题是线性问题,这种结构天然适合Benders。
第二是滚动时域(Rolling Horizon)。这是我在工程里更常用的办法:不一次性求解72个时段,而是每12个时段一个窗口,求解完把前4个时段的决策下发执行,然后窗口向前滚动4个时段重新求解。滚动时域不保证全局最优,但它把整数变量规模砍到原来的六分之一,还能在线更新故障信息——比如某条道路因为次生灾害中断了,下一轮优化立刻就知道。
滚动时域里有两个容易翻车的细节。一是窗口重叠长度:重叠太短会导致前后窗口决策不衔接,MPS在窗口边界附近反复横跳;重叠太长退化成全局求解。一般取窗口长度的1/3到1/2。二是求解完成后的“冻结”机制:已下发执行的决策要作为硬约束传给下一轮,不能让模型把过去时段改掉了。不写这个约束,滚动时域的结果会“追悔过去”,调度方案每一轮都不一样,现场根本没法执行。
5. MPS动态调度落地的5个典型坑:从现象到根因排查
5.1 现象:MPS还没到节点,计划里的恢复时间已经到了
第一次跑完动态调度模型,我盯着输出表发现MPS计划在t=2给节点24送电,但按道路距离和车速算,它t=4才能到。模型居然给出了一个“时间穿越”的方案。
原因:道路弧段的通行时间没有建模进z变量的可用时段里。我检查发现约束里只限制了“移动中和供电中不能同时发生”,但没有限制“还没到达就不能开始供电”。时间网络里漏了“可达性”约束。
解决:给每个MPS在每个节点的最早可用时段加一个下界。具体做法是先用最短路径算法算出预配置点到每个节点的最短通行时间,然后加约束:当 t < 最短通行时间 时,z[p][t][n] 强制为0。这个约束在优化前预处理就能做,不用增加整数变量。
5.2 现象:预配置方案和动态调度结果“打架”
灾前的预配置选了A点和B点,灾后动态调度却几乎不用A点的MPS,只调度B点的MPS。表面上看A点的预配置白做了,实际上问题出在两阶段模型的信息传递上。
原因:预配置阶段的目标函数里,每个故障场景的概率权重设置不对。低频高损场景权重太低,预配置优化就“放弃”了那个方向的覆盖;等这个场景真发生了,动态调度面对的是无解的局面。
解决:把场景概率和目标函数做成参数而不是写死。我一般先跑一版所有场景等权重的预配置,看哪些节点被选中;再把概率权重调成真实故障统计值,对比两次预配置的差异。如果差异太大,说明场景生成不充分,需要检查故障集是不是只考虑了N-1而忽略了N-1-1的级联故障。
5.3 现象:求解器报“infeasible or unbounded”
这个报错几乎是MPS调度建模的必经之路。我第一次看到这个提示直接懵了,因为模型是照着论文公式敲的,觉得逻辑没问题。实际上这个报错只说明求解器在预处理阶段发现模型既不可行也无界,具体原因还得自己查。
原因:最常见的是能量约束初始条件没给。soc[p][0] 没有赋值,能量平衡方程里的变量就自由了,求解器可以无限放电得到无界解,也可以怎么都不满足容量约束得到不可行解。另一个常见原因是MPS数量为0或者load字典里某个节点只有权重没有负荷功率,导致目标函数里出现 None 参与运算。
解决:先跑一遍“可行性诊断”。把目标函数临时设成常数0,只保留约束,看求解器能不能找到可行解。如果可行,说明约束没问题,问题出在目标函数;如果不可行,用Gurobi的IIS(不可行子系统)功能或者CBC 打印冲突约束来定位。这个诊断步骤值得写在代码注释里,省得下次再浪费两小时。
5.4 现象:单次故障场景下的最优调度,在故障演化场景里持续翻车
用单场景动态调度算出来的方案,投到实际故障演化(比如台风过程中线路一条接一条跳闸)里,MPS刚接好一个节点,另一个关键节点又失电了。调度方案跟不上故障动态。
原因:动态调度模型假设故障集合在决策时刻已经确定,属于开环决策。真实配电网里故障是演化的,部分线路会在不同时刻先后跳闸。开环方案对后续故障无法反应,现场只能靠人为干预,模型自然“翻车”。
解决:切换到闭环的滚动时域调度。每个控制周期(比如1小时)重新读取当前故障状态和MPS实际位置,更新负荷重要度参数后重新求解。这事第4章提到的滚动时域正好是同一个框架,只是把“预测故障”换成“实时故障更新”。注意重新求解时不要冷启动,把上一轮的MPS位置和电量作为初始条件输入,求解速度会快很多。
5.5 现象:重要负荷权重高,但调度结果里它反而被跳过了
我给医院节点设了权重10,结果调度方案把MPS派给了一个权重1的小用户,医院节点一直没恢复。目标函数明明在最大化加权恢复电量,怎么结果反了?
原因:负荷节点的“可接入性”约束没有建完整。医院节点虽然权重最高,但它的上级线路处于故障断开状态,MPS接入该节点后无法通过配电网向周围供电;或者该节点是孤岛节点,MPS容量不够支撑它的启动电流。换句话说,模型认为“接入了也没法供电”,所以干脆不选它。
解决:把“节点是否具备接入条件”作为参数预处理进模型。只有与故障隔离区域连通的节点才出现在z变量的候选集合里。另外检查一下负荷模型里是不是只写了稳定有功功率,没有考虑电动机启动时的冲击功率。有些重要负荷的启动电流是额定电流的5到7倍,MPS放电功率上限不足时,即使模型选了它,现场也会因为过流保护动作而送不上电。
细节排查经验:遇到这种情况,我习惯把调度结果导成Gantt图,横轴是时间,纵轴是MPS,颜色块表示接入节点。视觉上一眼就能看出“节点24被派了M2但M2根本没去”这类荒谬决策,比对着数字表定位快得多。
6. 验证与进阶:用滚动时域闭环仿真验证你的动态调度方案
6.1 从开环最优解到闭环仿真验证
很多论文里的动态调度验证只做一件事:给定故障场景,跑一次MILP,输出恢复电量。但实际项目里,这个“一次求解”的结果没有说服力,因为现场环境是时变的。我常用的验证办法是搭一个闭环仿真环境:环境模拟故障演化、MPS移动到位过程、负荷恢复状态;控制器每60分钟调用一次动态调度模型,输入当前状态,输出未来4小时调度计划,只执行前1小时。
闭环仿真跑完后,把三个指标打出来对比:总恢复电量、重要负荷累计停电时间、MPS平均利用率。我一般用这个验证过程来反推模型参数合不合理——如果MPS利用率低于40%,说明模型给的调度方案过于保守;如果高于90%,说明MPS一直在满负荷跑,故障蔓延时完全没有裕度。这两个极端都需要调参数。
6.2 三个值得反复调的参数
第一个是时间步长。步长1小时计算快,但MPS在两节点间移动用时只有20分钟时,1小时步长会让模型要么提前出发浪费供电时间,要么延后到达导致负荷等待。建议移动时间不是步长整数倍的场景,把步长设为15分钟。
第二个是负荷权重的量级设置。我踩过的坑是把权重直接按“医院10、学校5、居民1”设,表面合理,实际量级差异不够,目标函数里恢复电量(kW·h)本身是几百到几千的数量级,权重差异被淹没。正确做法是把权重乘到负荷功率上,让不同负荷的加权恢复电量有明显梯度差。
第三个是故障场景集的生成方式。只在单一场景下验证很容易过拟合,我一般从历史故障库抽样生成50个场景,跑预配置加滚动时域调度,取恢复电量的期望和90分位数来评估方案稳健性。
6.3 落地前的一个验证技巧和一句忠告
如果做的是科研复现,有一个高效的验证技巧:先拿一个已知最优解的小算例(比如3节点、2台MPS、4个时段),手算或穷举得到全局最优调度,然后看模型能不能复现。这个小算例要包含一次“MPS移动后供电”的动作和一次“MPS转移”的动作,两个动作都能复现,再往大系统扩展就不容易出原则性错误。
最后说个我自己的教训:MPS动态调度模型本身并不难写,难的是把配电网运行约束、交通约束和负荷时序约束放在同一个优化框架里不打架。我为此反复重写了三版数据接口,最后发现与其追求模型“一步到位”,不如从简化但闭环的版本跑通,再加约束。希望这个“先跑通再加密”的思路,能帮你在做配电网韧性提升和MPS动态调度时少走几段弯路。
本文还有配套的精品资源,点击获取