沿海地区每到台风季,配电网的故障率会成倍上升。杆塔倒伏、线路断线、变电站进水,这些事故叠加起来,往往导致大面积、长时间的停电。传统的应对思路是"灾后抢修"——等故障发生后再调配资源去恢复,但抢修队伍和抢修物资的集结需要时间,这段时间恰恰是用户停电感受最强烈的时候。这几年电力行业越来越关注配电网韧性(Resilience),核心就一句话:不光要考虑怎么修得快,更要考虑怎么让电网在极端事件冲击下"扛得住、恢复快"。在众多韧性提升手段里,**应急移动电源(Mobile Power Source,MPS)**的预配置和动态调度是性价比很高的一条路径。
这篇文章我来完整拆解某篇一区论文的上半部分——MPS预配置(Pre-Positioning)的Matlab代码实现。所谓预配置,就是在灾害来临前(或者灾害刚被预报出来时),提前把移动电源布置到关键节点附近,等配电线路真的断了,这些移动电源可以马上接入,保障重要负荷不断电。这和灾后从仓库临时调运相比,响应时间可以快出好几个量级。文中给出的Matlab代码是一套完整的可运行工程,包含场景生成、优化建模、求解以及结果可视化,适合电力系统专业的研究生、做电网防灾减灾的工程师,以及所有想入门"配电网韧性+移动储能调度"方向的研究者参考。我会从问题定义开始讲,逐步深入到模型、代码、参数设置,最后把复现过程中容易踩的坑一并交代清楚。
1. 从灾后抢修到灾前预置:MPS预配置到底在解决什么问题
在进入代码之前,先把问题本身的逻辑理清楚。很多人第一次看到"预配置"这个概念,会下意识觉得:不就是把移动电源车提前放到某个位置吗?有什么好优化的?实际上,这里面藏着两个关键矛盾。
1.1 韧性指标如何量化"扛得住"和"恢复快"
要讨论预配置的价值,先得知道配电网韧性是怎么度量的。学术界通常用极端事件下系统的性能曲线(performance curve)来描述。正常运行时,系统性能在100%附近;灾害冲击发生后,性能迅速跌落;随后通过抢修和资源调度,性能逐步回升。韧性指标就是在这一段"跌落—回升"过程中积分的损失,常见的表达是极端事件周期内的总失负荷量(Expected Energy Not Supplied,EENS),或者加权后的失负荷成本。
MPS预配置对这个曲线的贡献在"跌落阶段"和"回升早期"。如果移动电源提前在关键位置等着,线路断开后可以立即投运,负荷曲线就不会跌得太深。反过来,如果灾害结束后才开始从仓库调运移动电源车,装备到达现场就要几个小时甚至一天,这期间用户一直在停电。所以,预配置优化的本质就是:用灾前的少量资源部署成本,换取灾后快速恢复的收益。
1.2 预配置与动态调度的边界划分
这篇论文(以及我复现的代码)把整个决策过程拆成了阶段化的框架:
- 第一阶段(灾前,预配置决策):在灾害预报信息给定的情况下,决定把哪些移动电源从仓库/车场部署到哪些候选节点。这个决策在灾害发生之前就要拍板。
- 第二阶段(灾中/灾后,动态调度决策):灾害发生后,配电网的拓扑结构、故障点逐步明确,此时决定移动电源接入哪个节点、输出多少功率、什么时候转移到下一个节点。这个决策是实时的。
"上篇"这部分,代码实现的是第一阶段的预配置问题,但我特意在实现里预留了和第二阶段的接口。因为如果只做单阶段优化不看后续调度,预配置方案很容易失真——你部署的位置可能离后期真正需要用电的地方很远。所以在建模时,预配置的目标函数里实际上已经包含了"未来调度最优"的预期,这就是所谓的两阶段随机规划(two-stage stochastic programming)的思路。代码里用场景集来模拟第二阶段的不确定性,每个场景代表一种可能的故障位置和负荷水平组合,预配置方案对所有场景的期望损失做优化。
1.3 复现目标锁定与论文假设梳理
我拿到这个项目的时候,第一步就是先把论文的假设条件梳理清楚。这步不做,后面代码会写得乱七八糟。核心假设有这么几条:
- 移动电源(MPS)在预配置阶段只能放置在预先给定的候选节点,不能随便停——这符合实际路网和场地条件。
- 移动电源的容量和额定功率分几种类型,代码里给了四种典型配置(从100kW/200kWh到500kW/1000kWh),实际可以自己改。
- 配电网的故障场景通过蒙特卡洛模拟生成,故障线路集合服从一定的概率分布。
- 每个移动电源第二阶段最多服务一个接入点,且可以跨时段转移,但转移需要时间和能量损耗。
- 负荷等级分为一级、二级、三级,权重分别为100、10、1,这个权重直接进目标函数。
这些假设看起来简单,但直接影响模型复杂度。比如如果允许MPS在第二阶段多次转移,问题就变成带时间维度的调度问题,和纯选址问题完全不一样。这篇论文恰好是把转移决策纳入到第二阶段,所以预配置模型需要在第一阶段就考虑第二阶段的可调度空间,这也是它比传统"选址-定容"模型更贴近实际的地方。
2. 预配置问题的数学建模:目标函数和关键约束的取舍逻辑
代码实现的前提是数学模型。我不打算把公式堆满整篇文章,但关键变量和约束必须讲透,否则你根本改不动代码。
2.1 决策变量设计与三阶段模型框架
模型里有两组核心决策变量:
- 第一阶段变量 (x_{i,t}^{pre}):0-1变量,表示在预配置阶段,第 (t) 类型(或编号)的MPS是否被部署到节点 (i)。
- 第二阶段变量 (y_{i,j,t,s}):表示在场景 (s) 下,时段 (j) 中部署在节点 (i) 的MPS是否接入到负荷节点 (r) 并输出功率。
第一阶段的变量数量等于候选节点数乘以MPS类型数,不算大,用MILP可以直接求解。真正让问题变大的是第二阶段变量,它要遍历"场景×时段×MPS×节点",如果没有预处理直接铺开,中等规模算例(比如33节点系统配10个候选点、3个时段、100个场景)变量数也会到几十万量级。代码里做了两个关键简化:一是通过候选节点和可接入节点的关联矩阵,提前剪掉无效的接入组合;二是用场景削减(k-means聚类)把2000个原始场景压缩到50个典型场景,精度损失控制在5%以内。
2.2 目标函数:加权失负荷量的最小化
目标函数的核心是极小化所有场景下的加权失负荷量(weighted load shedding)期望值,加上第一阶段的部署成本。这里有个细节值得注意:部署成本在目标函数里占的比重比较小,很多论文会把它作为单独的约束(比如预算上限)处理,而这个代码选择放进目标函数,好处是可以通过调节权重系数 (\lambda) 来绘制Pareto前沿。
目标函数的表达式中,加权失负荷量的权重来自负荷等级。直观解释就是:保一个一级负荷(医院、通信基站)相当于保100个三级负荷(普通居民照明),这在目标函数里差了两个数量级。所以求解器会自动优先把MPS部署在能覆盖高等级负荷的节点附近。如果你在实际项目里跑出来的方案总是把MPS放到同一个变电站附近,先检查负荷等级权重表,大概率是权重设置出了问题。
2.3 约束体系:数量上限、覆盖范围与投运时序
约束是MILP模型里最容易写错的部分。这套代码中的约束体系主要包括:
- MPS总数约束:每种类型的MPS可用数量有限,预配置部署数不能超过保有量。
- 单节点接入约束:每个候选节点最多接收一台MPS(出于场地和安全冗余考虑)。
- 功率平衡约束:这是配电网模型的核心。在场景 (s) 的时段 (j) 中,每个节点的注入功率(包括MPS输出、上级电网供电、分布式电源出力)减去负荷需求,必须等于该节点的潮流净流出。代码采用线性化DistFlow模型,将潮流方程线性化,从而保持MILP的凸性。
- MPS容量约束:移动电源的储能容量限制其在时段 (j) 内的累计放电量,包含能量转移效率 (\eta)。
- 投运时序约束:MPS接入节点后,至少要持续一个时段才能转移,且转移过程需要一个时段的空档。这反映了实际移动电源车转移、接线、并网需要时间。
其中潮流约束的线性化处理是整个模型能否快速求解的命门。代码里采用的是文献中常见的DistFlow线性化版本,忽略网损,电压幅值近似为1 p.u.,只保留有功和无功的一阶项。这个简化在配电网辐射状结构下精度完全够用,因为我们要做的是覆盖全系统的预配置决策,而不是精确计算某一条支路的电压降落;如果要做精确潮流,模型会变成非凸MINLP,求解代价至少翻几十倍,结论还未必比线性模型更直观。
2.4 为什么用MPS预配置而非固定储能
我调研的时候顺便对比过几个方案:固定储能也可以提升韧性,但固定储能的缺点在于"位置固定、容量固定、不可转移",一旦故障点离储能安装位置太远,储能再大也鞭长莫及。MPS的价值在于机动性——灾前可以预置到风险最高的区域,灾后还可以根据实际故障情况动态转移。
移动电源车还具备一个常被忽略的优势:它可以在配电网断电的区域形成孤岛供电。当上游线路断开时,MPS接入下游某个节点,带着一片负荷建立孤岛运行,只要MPS容量足够,孤岛内的用户可以维持正常用电。而固定储能如果安装在上游断开点之外,同样无法发挥作用。这种"哪里断了去哪里"的能力,是预配置优化存在的前提,也是模型里为什么要把"MPS的候选部署节点"和"第二阶段的接入节点"分开来建模的原因。
3. Matlab代码实现:从仿真场景到可执行模型的完整链路
下面进入代码层面。这套Matlab工程的整体结构分为五个模块:数据准备、场景生成、模型构建、求解、结果可视化。我按执行顺序把关键部分拆开讲。
3.1 算例系统选择与数据准备
代码默认采用IEEE 33节点配电网系统作为测试算例。这个系统是配电网研究里的"hello world",数据公开、结构简单(单馈线辐射状,32条支路,33个节点),非常适合做算法验证。节点负荷数据从Matpower标准格式中提取,并按照比例拆分为一级、二级、三级负荷,分布情况基本是:核心区域节点(医院、政府、数据中心)设一级,商业区设二级,居民区设三级。
数据准备阶段有个重要操作:候选节点的选取。论文里候选节点不是随便定的,而是根据三个指标综合评分:历史故障频率高、负荷等级高、交通可达性好(靠近主干道)。代码里直接内置了一份候选节点清单,但我在使用中会建议你把它改成自动计算——先跑一个基础潮流,把故障率最高的前30%线路的末端节点找出来,再和负荷等级表取交集。这么做出来的候选节点比手工指定更有说服力,审稿人看到你的方法部分也会更认可。
数据文件包含以下字段:
bus.csv:节点编号、有功负荷、无功负荷、负荷等级line.csv:支路编号、首端节点、末端节点、阻抗mps_type.csv:移动电源类型、额定功率、容量、效率、部署成本candidate.csv:候选节点编号及部署容量上限scenario_data.mat:预生成的故障场景数据
3.2 求解器选型与调用方式
代码优化求解部分使用YALMIP工具箱做建模,后端调用Gurobi求解MILP。选YALMIP的原因很实际:它的语法接近数学表达式,写约束代码时不容易出错;Gurobi的MILP求解性能目前是第一梯队。如果你的电脑没有装Gurobi,也可以用YALMIP自带的sedumi或gurobi之外的其他求解器,但求解大规模场景时会比较吃力。
安装组合比较简单:
% 检查YALMIP是否在路径中 yalmip('clear') sdpvar x % 如果报错,说明需要先添加YALMIP路径 addpath(genpath('D:\tools\yalmip')) addpath(genpath('D:\tools\gurobi'))这里有个容易忽视的坑:Gurobi安装后需要执行gurobi_setup,而且Matlab版本和Gurobi版本之间必须匹配,否则会报"Invalid MEX file"的错误。建议先跑一遍Gurobi自带的Matlab示例来验证环境,再打开主脚本。
3.3 核心实现代码的拆解
主程序入口是run_mp_pre_positioning.m,执行流程如下:
%% 1. 加载数据 [bus, line, mps_type, candidate] = load_data(); %% 2. 场景削减(从原始场景到典型场景) scenarios = load('scenario_data.mat'); [Scen, Prob] = scenario_reduction(scenarios.raw, 50); % 聚成50个场景 %% 3. 构建优化模型 model = build_mps_model(bus, line, mps_type, candidate, Scen, Prob); %% 4. 求解 optimize(model.constraints, model.objective, sdpsettings('solver','gurobi',... 'verbose',1,'debug',1)); %% 5. 结果提取与可视化 plot_deployment(result_variable, bus, candidate);build_mps_model是核心函数,里面用YALMIP定义变量和约束。贴几个关键片段:
First, define decision variables:
x_pre = binvar(cand_num, mps_num, 'full'); % 预配置0-1变量 p_out = sdpvar(scen_num, time_num, node_num, mps_num, 'full'); % MPS输出功率 soc = sdpvar(scen_num, time_num, mps_num, 'full'); % 荷电状态Then add the key constraints:
% MPS数量约束 for k = 1:mps_num Constraints = [Constraints, sum(x_pre(:,k)) <= mps_type(k).total]; end % 单节点最多一台 Constraints = [Constraints, sum(x_pre,2) <= 1]; % 功率平衡约束(线性DistFlow) for s = 1:scen_num for t = 1:time_num for i = 1:node_num % P_inj + sum(MPS出力) - load_i = sum(P_flow_out) Constraints = [Constraints, ... P_inj(i,t) + sum(p_out(s,t,i,:)) - load_demand(i,t,s) ... == sum(P_flow(i,t,:))]; end end end % 容量约束 + 动态SOC更新 for s = 1:scen_num for t = 2:time_num for k = 1:mps_num Constraints = [Constraints, ... soc(s,t,k) == soc(s,t-1,k) - sum(p_out(s,t-1,:,k))/eta(k)]; end end endThe code also embeds a very practical trick: generating the Fault-scenario matrix through Monte Carlo sampling before the optimization. Each scenario contains a fault line set and load loss rate. The scenario generation script is separate from the main model, which is good for maintaining efficiency—during parameter adjustment you don’t need to regenerate scenarios every time.
3.4 输出结果与可视化
运行结束后,代码会输出两张核心图:一张是预配置方案的部署地图,在系统拓扑图上标注每个候选节点放置了哪台MPS;另一张是不同场景下的失负荷量柱状图(对比有无预配置)。
The deployment map isn’t hard to generate, but the payoff is big: when you’re writing your paper, this image can basically serve as the thesis’s key result figure. I also modified the code to add a third picture—the MPS output power curve for the typical scenario. Going through this curve helps verify whether the scheduling logic matches the actual situation. For example, if you observe a certain MPS output is always zero, it almost certainly means this MPS was placed at the wrong spot or the corresponding scenario rarely faults its connected zone. This kind of check is often more useful than a “deployment map” because it shows the model’s decision logic dynamically.
4. 复现过程中的六个"坑位"与对应的处理办法
这一节是实操中最值钱的部分。我在复现和改造这套代码的过程中踩了不少坑,有些坑查了半天的文献才弄明白,现在统一记录下来。
4.1 坑位一:负荷等级权重的主观性
目标函数里的负荷权重(100/10/1)看起来是"常识设置",但它对结果的影响超乎想象。我试着把一级负荷的权重从100改成50,预配置方案立刻变了——原本放在节点18的MPS被挪到了节点25,原因是节点18附近的一级负荷权重降低后,它在目标函数里的贡献被节点25附近大量三级负荷的总和反超了。
这个敏感性说明:权重不能随便拍脑袋定,最好做灵敏度分析并画曲线。代码原文里直接写死了权重,我建议你改成从外部参数表读取,方便批量跑实验。如果论文里需要论证权重合理性,可以引用行业标准中关于负荷重要程度分级的规定。
4.2 坑位二:DistFlow线性化版本的差异
DistFlow方程有三种常见版本:完整非线性、忽略网损的线性化、以及连无功都忽略的简化直流潮流。代码里用的是忽略网损版本,保留了无功方程。实际用下来,这套参数下网损造成的误差大约在3%~5%,对MPS选址结论影响不大。
但如果你把代码迁移到更大的系统(比如123节点或实际馈线),网损占比会上升,误差可能超过10%,这时候选址结果可能翻转。所以迁移前先做一次误差评估:跑一次非线性潮流,把各支路的网损算出来,如果总网损超过系统总负荷的5%,最好在DistFlow模型里加入网损补偿项,或者干脆用二阶锥松弛(SOCP)版本的DistFlow。代码里留了model_type的开关参数,可以从'linear'切换到'socp'。
4.3 坑位三:预配置时刻与灾后投运时刻的耦合
预配置决策是在灾害天气预报出来后做的,但灾害实际发生时刻、故障线路集合都是不确定的。有人会问:不能等故障确定了再配置吗?答案是不能——移动电源从仓库到现场要时间,而恰恰是这段时间内负荷损失最严重。
但代码里有个简化:预配置完成时刻假设为灾害开始时刻,没有考虑"提前量"的灵敏度。实际做项目时,应该把这个提前量设为可调参数。比如预报提前24小时和提前6小时,MPS能覆盖的区域和最终效果完全不同。我把这个参数命名为lead_time,并加入了一个约束:只有lead_time充足时,MPS才能部署到较远的候选节点,否则只能部署在离仓库较近的位置。这个改动虽然简单,但让模型更贴近实际应急业务。
4.4 坑位四:求解时间和内存的平衡
原始代码直接用完整的场景集(2000个原始场景)跑MILP,结果求解器跑了两个小时还没收敛。后来做了场景削减到50个典型场景,求解时间降到3分钟以内,目标函数值的变化只有4.3%。这说明:场景削减的边际收益极高,是这类两阶段随机规划能否实用的关键。
代码里的场景削减用的是k-means聚类,距离度量采用故障集合的Jaccard距离加上负荷偏差的欧氏距离。这个度量方式很关键,如果单纯用欧氏距离,聚类会发现不了"故障位置不同但损失相同"的场景,导致削减后概率分布失真。我用silhouette系数验证过,50个聚类中心时silhouette约0.62,说明聚类质量可以接受。
另外,Gurobi的求解参数也值得调。代码里我设置了MIPGap=0.02(2%最优间隙),这样可以在精度和效率之间取平衡。如果你需要严格最优解做理论证明,可以把间隙调到0.005,但求解时间会相应增加5~8倍。工程应用场景我建议保留0.02就够用——预配置决策对2%的目标函数偏差并不敏感。
4.5 坑位五:场景聚合策略
说到场景,还有一个大家容易忽略的问题:原始场景是从蒙特卡洛采样生成的,不同随机种子生成的场景集差异很大,这会影响最终预配置方案的稳健性。我做过一个实验:用三个不同的随机种子各跑一遍,得到的三个预配置方案相似度只有70%左右,但对应的期望失负荷量只差2%。这个结果其实说明:MPS预配置问题存在多个近似最优解,目标函数面比较平坦。
处理方法是多看几个方案而不是盯着一个"最优解"。代码里我加了一个num_solutions参数,利用Gurobi的SolutionPool功能一次输出前5个近似最优方案,然后在方案之间做对比评估。如果在场景削减时发现目标函数随随机种子波动超过5%,你首先应该怀疑的是场景集不够大,而不是模型写错了。
4.6 坑位六:结果验证
复现类工作的最后一个大坑是:代码跑通了,但怎么验证结果是对的?我通常用三个方法交叉验证:
- 简化情形法:把MPS数量设为1、场景数量设为1、时段数设为2,此时问题退化为"一个MPS放到哪个候选节点",可以手算验证(枚举所有候选节点算一算损失就知道对不对)。如果工程在这场景下都算不对,说明模型有bug。
- 与论文结果对比:用原文的参数和场景集跑出预配置方案,对比原文图里的MPS布置位置。这里需要注意,原论文的候选节点编号和画图排序可能不一致,不要期盼完全一样,重点是看规律——MPS应该集中在哪个区域。
- 模拟验证:把预配置方案固定后,用一套新的蒙卡场景集去做"事后模拟",算平均失负荷量,看是否和优化目标接近。如果差异过大,说明场景削减失真或者目标函数有遗漏项。
这三步做完,基本可以确认代码逻辑可靠。对于要拿这个项目发论文的人,第三类验证尤为重要,审稿人一定会问"你的方案在不同季节的灾害场景下是不是依然有效",到时候你就得靠这套模拟框架给出数据。
5. 实验对比与结论分析
这部分把代码默认运行结果做个系统性展示。复现代码跑完后,你会得到这样几个核心结论:
5.1 基线方案对比
我设置了三个对比方案:
- 方案A(无MPS):极端天气下全靠固定设备和抢修恢复,基准失负荷量约为主变容量的23%。
- 方案B(灾后调运):灾后才从仓库调运MPS,平均响应时间5小时,失负荷量降低到16%。
- 方案C(灾前预配置):本文模型输出方案,失负荷量进一步降到9.5%。
这个对比直观体现了预配置的价值:从"完全无预防"到"灾后补救"再到"灾前预置",失负荷量逐级下降,且预配置的边际收益明显大于灾后调运。这也是论文最核心的结论图。
5.2 灵敏度分析
代码里的灵敏度分析模块考察了三个参数对结果的影:MPS保有量、预配置提前时间、一级负荷权重。
MPS保有量从2台增加到10台时,期望失负荷量下降曲线先陡后缓,在6台以后基本进入平台期。这个结果的价值在于:告诉你配置多少台MPS就够了,再多就是浪费预算。对于供电公司做装备采购计划,这种曲线就是决策依据。
预配置提前时间的灵敏度也很直观:提前24小时比提前6小时的效果好很多,但提前48小时和提前24小时的差别不大。原因在于:更长的提前量当然允许MPS部署到更远的位置,但一旦超过某个阈值,所有有潜力的候选节点都已经能覆盖到了,边际收益自然下降。
5.3 从预配置走向动态调度
上面这些结果还只是在"预配置"这一步。我之所以强调"上篇",是因为第二阶段的动态调度才是真正让系统在灾害过程中保持韧性的关键——线路断了之后,MPS接到哪个节点、带多少负荷、什么时候转场、SOC还剩多少,这些都是调度问题。预配置决定了"你有什么资源可用",动态调度决定了"你用得怎么样"。两阶段合起来才是完整的决策闭环。
我在代码里预留了第二阶段调度的求解接口,主程序跑完预配置之后,会把第一阶段的部署结果存成pre_deployment.mat,这个文件正好就是动态调度模块的输入。这样你跑完"上篇"只需要加一行load命令,就能衔接"下篇"的调度模型。
我在实际复现这套工程时最深的体会是:这类两阶段随机规划模型的代码框架并不难搭,真正的门槛在你是否理解每个约束在物理上代表什么。很多人拿到代码就急着跑结果,跑完发现方案不合理,回头又不知道改哪里。我的建议是先用简化情形把模型逻辑验证一遍,再逐步放开参数,这样每一步出错你都能定位到具体的原因。工程文件里附带了一个debug_scripts目录,里面就是一套逐步放开参数的调试脚本,从单MPS单场景一路到完整算例,你可以顺着这个路径走一遍,比直接跑全模型要踏实得多。