提到“配电网韧性提升”和“应急移动电源预配置”这组关键词,业内人应该不陌生。这几年极端天气频发,配电网大面积停电的教训太多了,单纯靠加固线路与变电站,投资巨大而且响应速度跟不上。于是移动电源这类灵活性资源被推到台前——平时可以当储能参与运行,灾后又能开到关键节点给重要负荷供电。但这里藏着一个决策难题:灾前把MPS(Mobile Power Source,应急移动电源)预布置在哪里?数量怎么定?灾后才好快速响应、最大化恢复负荷?这篇要聊的SCI一区复现项目,正是聚焦“上篇”——MPS预配置阶段,Matlab代码完整实现。适合正在做配电网韧性、虚拟电厂、分布式资源调度方向的研究生,或者想从学术模型过渡到可运行代码的工程师参考。下文把模型数学原理、Matlab整体架构、关键函数实现、求解配置与调参细节逐一拆开讲透。
1. 项目定位与核心问题拆解
1.1 为什么先从“预配置”阶段下手
“韧性”(Resilience)在电力系统的定义,指的是系统面对极端扰动时,能够提前预防、实时抵御、事后快速恢复的综合能力。相比传统可靠性强调故障概率,韧性更看重事件的极端性和恢复时间。同样是切负荷,常规N-1故障切掉一个用户半小时,和台风过后整片中压馈线停运三天,量级完全不同。
MPS的完整决策链条实际上包含两个时间层级:第一阶段在灾前决定移动电源的初始部署位置和容量配置;第二阶段在灾害发生后,根据实际故障场景,决策MPS从初始节点向目标节点移动的路径与并网出力。这两个阶段紧密咬合——灾前位置选偏了,灾后跑过去可能路都断了;容量配少了,关键负荷照样黑灯。
本项目把复现内容切出“上”“下”两篇,也是这个逻辑。“上篇”的MPS预配置,是整条链路的战术起点:不解决“把鸡蛋放在哪几个篮子里”的问题,后面动态调度再精巧也是空中楼阁。
1.2 预配置问题的真实工程约束
预配置模型不是简单的选址问题,实际得同时处理三类约束:
- 空间约束:MPS只能布置在具备接入条件的节点,比如有变电站出口、开闭所或柱上变的位置,不能随便搁在某个台区。
- 时间约束:灾害预测信息有限,从预警发布到灾害抵达之间的准备窗口可能只有几个小时,预配置方案必须在窗口内可执行。
- 资源约束:移动电源车队规模有限,每台MPS的容量固定,极端场景下还要考虑不同重要等级的负荷差异化恢复。
这三类约束叠加后,模型天然具备混合整数非线性规划的特征。这也是为什么我建议直接用YALMIP这类建模工具箱来做——手写求解器容易把自己绕进数值坑里。
1.3 复现前需要建立的“先验地图”
动笔写代码前,先把文献里常用的数学符号和模型形式过一遍,后面再对应Matlab实现就不会乱。通常定义如下:
- 配电网用辐射状图表示:节点集合 ( \mathcal{N} ) ,支路集合 ( \mathcal{E} )
- MPS配置决策变量:( x_{i,s} \in {0,1} ),表示第 ( s ) 台MPS是否在节点 ( i ) 预部署
- 负荷削减变量:( \Delta P_{i,t} ) 和 ( \Delta Q_{i,t} ),表示节点在时段 ( t ) 的有功/无功削减量
- 目标一般是极小化“预配置成本 + 期望切负荷惩罚成本”,切负荷惩罚系数按负荷重要等级分层设置
这部分模型框架理清楚之后,再看代码结构,基本就是“翻译”工作。
2. MPS预配置模型的数学原理与求解关键
2.1 目标函数里藏着权衡逻辑
预配置阶段的目标函数通常长这样:
[ \min ; \sum_{s \in \mathcal{S}} \sum_{i \in \mathcal{N}} C^{\mathrm{pre}}{i,s} x{i,s} + \sum_{\omega \in \Omega} p_\omega \sum_{t \in \mathcal{T}} \sum_{i \in \mathcal{N}} C^{\mathrm{cut}}{i} \Delta P{i,t,\omega} ]
第一项表示MPS预部署产生的成本,可能是租赁费、运输预备费,也可以是折算后的日均成本。第二项是场景 ( \omega ) 下切负荷的期望惩罚。这里的核心权衡是:多布一台MPS,灾前要多花一笔钱;但灾后能多恢复一片负荷,惩罚成本下降。最优解恰好落在两者边际相等的点上。
实操中要注意单位统一。文献里负荷单位常用kW或MW,MPS容量是kWh或MWh,时间尺度一般是1小时一个时段。如果惩罚成本按“元/kWh”计,而预配置成本按“元/台”计,那目标函数第一项和第二项的量纲就要小心处理,否则求解器会给出匪夷所思的极端解。我在复现时就曾因为把容量单位写成kWh、惩罚系数却按MWh标定,结果求解出来一台MPS都不配,整片负荷全切了——明显是量纲陷阱。
2.2 DistFlow潮流方程与二阶锥松弛
配电网潮流计算和输电网不同,线路电阻不小,不能简单用直流潮流。预配置模型里用得最广的其实是DistFlow方程:
[ P_{ij,t} - r_{ij} \tilde{l}{ij,t} = \sum{k: j \to k} P_{jk,t} + P_{j,t}^{\mathrm{load}} - P_{j,t}^{\mathrm{gen}} - P_{j,t}^{\mathrm{MPS}} ]
[ Q_{ij,t} - x_{ij} \tilde{l}{ij,t} = \sum{k: j \to k} Q_{jk,t} + Q_{j,t}^{\mathrm{load}} - Q_{j,t}^{\mathrm{gen}} - Q_{j,t}^{\mathrm{MPS}} ]
其中 ( \tilde{l}_{ij,t} ) 是支路电流幅值平方的松弛变量。直接求解仍然非凸,因为还有电压和电流的耦合约束:
[ \tilde{l}{ij,t} \geq \frac{P{ij,t}^2 + Q_{ij,t}^2}{V_{i,t}^2} ]
这一步就得靠二阶锥松弛(SOCP relaxation)把非凸约束转成凸锥约束,再交给商业求解器处理。绝大多数SCI一区文献用的是这种处理方式,少数用线性化DistFlow。复现时建议先做SOCP版本,精度好;如果求解时间扛不住再退化到线性化版本做对照。
2.3 辐射状网络的生成树约束
配电网正常运行时必须保持辐射状,这在优化模型里是个麻烦的拓扑约束。常用做法是引入“生成树”约束,即每个非电源节点有且仅有一条父支路,且连通性由单商品流约束保证。
在Matlab里,这个约束通常写成矩阵形式:
- 节点-支路关联矩阵 ( \mathbf{A} )
- 选择变量 ( z_{ij} \in {0,1} ) 表示支路是否处于连通状态
- 生成树约束:( \sum_{j} z_{ij} = 1 )(除了根节点),以及容量辅助变量的流量约束
复现时,如果配电网规模是IEEE 33节点或123节点系统,手写关联矩阵还能接受;如果做到几百上千节点,建议直接用图论工具箱生成。
2.4 大M法处理MPS接入状态的乘积项
预配置阶段有个典型的非线性来源:MPS是否在节点接入、以及接入后出力是多少,两者是乘积关系。假设 ( u_{i,t} ) 是MPS在时段 ( t ) 对节点 ( i ) 的供电状态,那 ( P^{\mathrm{MPS}}{i,t} = u{i,t} P^{\mathrm{cap}} ) 这类约束无法直接进MILP。
处理思路是用大M法拆解:
[ 0 \leq P^{\mathrm{MPS}}{i,t} \leq P^{\mathrm{cap}} \cdot y{i,t}, \quad \sum_i y_{i,t} \leq S ]
这里 ( y_{i,t} ) 表示MPS在时段 ( t ) 是否接在节点 ( i )。预配置决策 ( x_{i,s} ) 和时段接入变量 ( y_{i,t} ) 之间还要有逻辑约束——如果某台MPS没被预先部署在某节点,该节点就不能接入该台MPS。大M的参数选择有个讲究:太小可能误伤可行解,太大会让线性松弛很松、分支定界效率骤降。一般取负荷总量的量级,再乘1.2~1.5倍作为安全系数。
3. Matlab实现环境与整体代码架构
3.1 工具箱与求解器选型
复现这个项目,环境准备清单如下:
| 工具 | 推荐选择 | 作用 |
|---|---|---|
| MATLAB版本 | R2020b及以上 | 对YALMIP和Cplex的兼容性更好 |
| 建模工具箱 | YALMIP(最新版) | 把你的模型从数学语言“翻译”成求解器能懂的标准形式 |
| 求解器 | IBM CPLEX 或 Gurobi | 解决MILP/SOCP问题的核心引擎 |
| 数据系统 | IEEE 33节点或IEEE 123节点标准算例 | 配电网测试系统的标准数据 |
提示:CPLEX对二阶锥约束的支持比较成熟,但注意许可证类型。学术版与商用版的求解规模限制不同。用Gurobi也可以,模型代码不用大改,YALMIP底层会做适配。
安装时最容易被忽略的是编译环境。YALMIP本身是纯Matlab代码,但调用Cplex时需要Matlab能识别Cplex的动态链接库。在Matlab里运行yalmiptest,如果全部通过,说明环境配置正常。我见过不少人在这一步卡住,最后发现是路径没add到Matlab搜索路径。
3.2 代码模块划分与文件结构
一个适合复现与二次开发的工程结构我推荐这样组织:
MPS_PreAllocation/ ├── main.m % 主程序:数据加载、模型求解、结果输出 ├── data/ │ ├── case33.mat % 节点/支路参数、负荷曲线 │ ├── scenario_data.mat % 极端事件场景集 │ └── mps_params.m % MPS容量、数量、成本参数 ├── functions/ │ ├── build_network.m % 构建节点-支路关联矩阵 │ ├── build_scenarios.m % 场景生成与概率赋值 │ ├── formulate_model.m % YALMIP建模核心 │ ├── add_socp_constraints.m % DistFlow与SOCP约束 │ ├── add_topology.m % 辐射状拓扑约束 │ └── plot_results.m % 结果可视化 └── output/ └── results.mat % 保存决策变量与目标函数值这个结构的优势在于:数据和模型逻辑完全分离,后面换IEEE算例或改场景规模,只需要替换data目录文件,不用动建模代码。
3.3 主程序骨架
main.m的骨架大致长这样:
%% 初始化 clear; clc; close all; addpath('functions'); load('data/case33.mat'); run('data/mps_params.m'); %% 构建场景 scenarios = build_scenarios(case_data, scenario_config); %% 建模与求解 model = formulate_model(case_data, mps_params, scenarios); options = sdpsettings('solver', 'cplex', 'verbose', 2, ... 'debug', 1, 'savesolveroutput', 1); sol = optimize(model.F, model.obj, options); %% 结果处理 if sol.problem == 0 [x_opt, y_opt, obj_val] = extract_result(model, sol); plot_results(case_data, x_opt, y_opt, obj_val); else disp(['求解失败,错误码: ', num2str(sol.problem)]); end核心在formulate_model里面,那段代码承担了把第二节的数学约束翻译成YALMIP语法的所有工作。
3.4 YALMIP建模的典型写法
以目标函数为例,YALMIP里的写法非常接近数学表达式:
%% 决策变量 x_pre = binvar(n_node, n_mps, 'full'); % 预配置位置 y_con = binvar(n_node, n_mps, n_scene, 'full'); % MPS接入状态 p_cut = sdpvar(n_node, n_time, n_scene, 'full'); % 切负荷量 %% 目标函数 obj = sum(sum(mps_pre_cost .* x_pre, 2), 1); for w = 1:n_scene obj = obj + prob(w) * sum(sum(cut_cost .* p_cut(:,:,w), 2), 1); end注意binvar和sdpvar的维度顺序要跟后面的约束保持一致。最容易出错的地方是三维变量的索引——Matlab切片和YALMIP内部线性化索引不一致时,约束会莫名其妙错位,而且错误信息不明显。我的经验是先写一个二维小规模测试算例,逐项检查约束大小,再上完整场景。
4. 核心环节的完整复现路径
4.1 场景生成:极端事件不确定性建模
预配置模型必须有场景支撑,因为“灾前不知道哪里会断”这件事本身就是模型的一部分。场景生成的常见方法有三种:
- 蒙特卡洛抽样:按历史故障概率对每条馈线段抽样,生成故障场景集合。
- 典型场景聚类:用K-means等聚类算法把上千个随机场景压缩成十几个有代表性的典型场景,并重新分配概率。
- 鲁棒边界法:不显式枚举场景,而是用不确定性集合描述故障位置与故障持续时间。
复现SCI一区文献时,多数用的是聚类压缩后的场景集。build_scenarios.m这个函数做的事情就是:读取线路历史故障概率→生成大量样本→聚类→输出典型场景和对应概率。
这里有一个数值细节:聚类后场景概率之和必须归一化到1,否则目标函数第二项的期望权重会出现系统性偏差。我调试时遇到过目标值偏大、但解却不合理的情况,最后检查发现场景概率最大的一项只有0.7,其余概率没有归一化。
4.2 DISTFLOW约束与SOCP的正式落地
在add_socp_constraints.m里,YALMIP处理二阶锥约束的推荐做法是使用cone命令:
for t = 1:n_time for i = 1:n_branch % P_ij^2 + Q_ij^2 <= V_i^2 * l_ij F = [F, cone([P_branch(i,t); Q_branch(i,t)], V_node(from(i),t))]; F = [F, V_node(to(i),t) == V_node(from(i),t) - 2*(r(i)*P_branch(i,t) + x(i)*Q_branch(i,t))]; end end如果不使用cone,也可以直接写P^2 + Q^2 <= V^2 * l,YALMIP会自动检测凸二次约束并转换为二阶锥形式。不过显式用cone更稳,求解器内部的预处理器更容易识别。
这里特别提醒一个物理合理性检查:求解完成后要回代验证电压幅值约束是否在允许范围内。如果SOCP松弛不紧,会出现“假可行解”——目标值看起来很好,但电压已经低于0.9 p.u.。处理办法是在目标函数中给电压偏差加一个小惩罚项,或者用迭代收紧法(先求解,再把对偶乘子加到目标里重新求解)。
4.3 辐射状拓扑约束的具体实现
辐射状约束我建议用单商品流方法,而不是显式的环消除约束。原因很简单:环消除约束在规模变大时数量爆炸,而单商品流只需要在每条支路增加一个连续辅助变量,约束数量线性增长。
核心逻辑如下:
%% 每个非根节点有且仅有一个父支路 F = [F, sum(z_parent(:, 2:end), 1) == 1]; %% 单商品流:根节点注入虚拟流量,逐节点消纳 F = [F, sum(flow(:, 2:end), 1) - sum(flow(:, 1:end-1), 2) == 1]; F = [F, flow >= 0, flow <= n_node * z_parent];其中大M系数可以直接设为节点数 ( n_node ),因为流动量最大不会超过节点总数。这里有一个细节:z_parent的关联方向和flow的方向定义要一致,否则约束会自相矛盾。建议在构建build_network.m时统一按“节点编号小的一端到编号大的一端”作为正方向。
4.4 MPS容量与接入逻辑约束
这部分是预配置模型的“灵魂”。约束逻辑如下:
- 每台MPS最多预配置在1个节点:( \sum_i x_{i,s} \leq 1 )
- 每个节点最多接入限定数量的MPS:( \sum_s y_{i,t,s} \leq M_i^{\max} )
- 灾后只有预配置过的MPS才能接入节点:( y_{i,t,s} \leq x_{i,s} )
- MPS输出功率上限:( P^{\mathrm{mps}}{i,t,s} \leq P^{\mathrm{cap}}{s} y_{i,t,s} )
- 所有MPS总输出功率限制:( \sum_i P^{\mathrm{mps}}{i,t,s} \leq P^{\mathrm{cap}}{s} )
写成YALMIP约束大致是这个样子:
for s = 1:n_mps F = [F, sum(x_pre(:,s)) <= 1]; for w = 1:n_scene for t = 1:n_time F = [F, sum(y_con(:,s,t,w)) <= 1]; for i = 1:n_node F = [F, y_con(i,s,t,w) <= x_pre(i,s)]; F = [F, p_mps(i,s,t,w) <= cap(s) * y_con(i,s,t,w)]; end end end end注意:如果MPS可以在灾区移动,则y_con可以随时间变化;如果预配置后固定接入不动,那y_con不随时间变化,模型规模会小很多。复现之前先确认文献里用的是哪种假设,这直接影响决策变量维度和求解难度。
4.5 求解配置与参数调优
模型写完后,求解器参数对能否在可接受时间内收敛至关重要。我这里给一组实测下来比较稳的设置:
options = sdpsettings('solver', 'cplex', ... 'cplex.mip.tolerance.mipgap', 0.01, ... % 1%的gap就收手 'cplex.mip.tolerance.absmipgap', 1e-3, ... % 绝对值gap 'cplex.mip.limits.timelimit', 3600, ... % 时间上限 'cplex.mip.threads', 4, ... % 并行线程数 'verbose', 1);1%的MIP gap不是随意设的。SCI文献里很多结果图的纵轴误差条其实就来自这个gap——你不需要把目标函数算到小数点后第6位,实际工程决策差1%根本不可见。如果真的需要紧解,可以分两轮:第一轮粗搜拿初始解,第二轮把tolerance调到0.01%并延续热启动。
如果场景数太多导致求解超时,有几个降维手段:
- 把聚类场景数从50压到15,观察目标值变化;通常15个典型场景就能覆盖90%以上的不确定性信息。
- 对MPS接入点在预配置阶段做对称性破缺约束,减少分支对称性。
- 把时间层聚合:非故障时段用1小时分辨率,故障恢复关键时段用15分钟分辨率。
5. 复现过程中的常见问题与排查技巧实录
5.1 求解器报错“INFEASIBLE”的排查顺序
这是复现时最让人崩溃的问题之一。YALMIP返回problem=1(infeasible),说明模型连初始可行解都找不到。按以下优先级排查:
- 检查节点与支路编号连续性:IEEE算例导出的数据经常有“跳号”节点,导致关联矩阵错位。
- 检查负荷与电源平衡:更新后的总负荷是否超过变电站容量加MPS总容量之和。
- 检查辐射状约束:如果根节点选错,或多个根节点同时存在,必然不可行。
- 检查大M参数:把大M值改大再试,排除因M系数过小导致合法路径被切断的情况。
提示:给
optimize的第三个参数sdpsettings('debug', 1)打开后,YALMIP会尝试定位不可行约束的准确位置。这是我最常用的调试手段,强烈建议每次排查都先开这个开关。
5.2 SCIP/CPLEX退出码非零的常见原因
用CPLEX求解时,返回码非零往往不是“模型错了”,而是数值问题。典型的错误码和处理方式如下:
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
Out of memory | 场景数太多,模型规模过大 | 减少场景,或使用延时约束生成法 |
Numeric difficulties | 大M值过大,矩阵条件数恶化 | 缩小大M,对变量做归一化处理 |
Time limit exceeded | MIP搜索空间爆炸 | 调大MIP gap,或增加初始可行解 |
Non-convex QP | 二阶锥约束被错误地写成非凸二次项 | 改用cone显式表达 |
5.3 求解完成但回代校验不满足物理约束
这个问题比不可行更隐蔽。目标值正常、变量也解出来了,但回代DistFlow发现电压越限或潮流不收敛。八成是SOCP松弛不紧导致的假解。
修复办法有三种:一是给目标函数加一个非常小的“惩罚项”鼓励松弛收紧,比如 ( 10^{-4} \times \sum (V_i^2 \cdot l_{ij} - P_{ij}^2 - Q_{ij}^2) );二是直接检查每条支路的松弛间隙,把间隙最大的支路单独加大权重重新迭代;三是改用凸包线性化DistFlow,牺牲少量精度换物理可行性。
复现时如果看文献里写了“SOCP relaxation is exact”,不要盲目相信,那是针对特定测试系统验证过的性质。换一套负荷数据,紧性可能就没了。
5.4 结果画图与对比的呈现建议
一篇合格的复现博文,除了代码能跑,还得给出让人看了就懂的可视化。我常用的画图方案包括:
- 配电网节点图叠加MPS预配置位置:红色标记预配置节点,蓝色标记重要负荷节点。
- 目标函数值随MPS数量变化的折线图:横轴为MPS台数,纵轴为总成本,可以清晰看到边际收益递减的拐点。
- 热力图展示故障场景下各节点的切负荷深度:直观对比是否“恰好避开重要负荷”。
plot_results.m里的核心思路就是这些。如果你想把结果做进论文里,建议Matlab画完后导出SVG或矢量PDF,再用绘图工具微调,效果会比直接截屏好很多。
6. 个人实操总结
复现SCI一区文献的预配置模型,踩过最深的坑就是“数学很美,代码很乱”。真正能落地的实现,关键是三点:一是把场景数据处理干净,二是把约束按拓扑、潮流、资源三个维度分开写,三是求解器参数要按模型规模调整而不是一套参数通吃。这套Matlab版本的MPS预配置代码,我已经在IEEE 33节点和部分改进的123节点系统上验证过,中等规模下把MIP gap设为1%时,求解时间通常在几分钟到十几分钟之间,完全够科研迭代用。动态调度那部分涉及灾后MPS的路径优化与时序耦合,等下一篇再展开。如果你按这里的步骤自己复现一遍,遇到某个约束报错或者结果对不上,欢迎带着你的报错信息和算例参数来交流,很多时候问题出在数据预处理上,而不是模型本身。