复现过配电网韧性那套框架的朋友应该都有体会,灾前预配置移动电源(MPS)只是第一步,真正让系统在故障后“活过来”的,是灾后那几十个小时里的动态调度。博文标题里说的“下篇——MPS动态调度”,就是整个两阶段框架里最考验建模功力的部分:MPS什么时候出发、去哪条线路、接在哪个节点、充多少放多少,每一步都牵扯到配电网的拓扑约束、负荷时序、电池电量和求解规模。这篇我就把自己复现这套SCI一区框架“下篇”的完整过程讲透,包含模型怎么拆、Matlab代码怎么组织、求解器怎么调、踩过哪些坑,给正准备动手复现或者做相关毕设/课题的朋友一条能直接抄作业的技术路线。
1. 先从整体盘盘:这套动态调度到底在做什么
1.1 从预配置到动态调度:两阶段逻辑的接棒关系
“上篇”做的是灾前预配置,也就是在极端事件还没发生、但预报已经给出故障场景集的时候,决策者需要确定:哪些站点要布置MPS、每个MPS多大的容量、初始停靠在哪里。这个阶段的输出是一组静态的“兵力部署方案”,本质上是带有随机场景的选址-定容问题。
“下篇”的动态调度则完全不同,它是在故障已经发生、配电网拓扑已经损坏、部分负荷已经失电的时间窗口内,对MPS做实时或滚动式的移动、接入、充放电管理。预配置解决的是“我有哪些资源”,动态调度解决的是“这些资源现在怎么用”。两者如果割裂开做,预配置的保守性会浪费MPS容量,动态调度的灵活性又无法充分体现,所以好的两阶段框架一定是用预配置的边界条件把动态调度约束起来,同时动态调度的预期收益又反过来指导预配置的决策。
复现时我建议先把这条“接棒关系”梳理清楚,否则很容易出现代码里预配置结果导不进来、动态调度变量对不上的问题。上篇跑完之后,输出的MPS初始位置矩阵、容量向量、可用数量,都是下篇动态调度的输入参数,缺一个转不起来。
1.2 动态调度要解决的几个核心矛盾
动态调度本质上是在一个不断变化的故障环境下做“时空联合优化”。我把它拆成三个矛盾,写代码和调模型之前先想清楚这三条,后面很多细节就顺了:
第一,MPS“移动”和“供电”的互斥。MPS不是无人机,它从一个节点撤出、沿道路行驶、再到下一个节点接入,这段时间内是不能给负荷送电的。这个“撤出—行驶—接入”的时间窗如果建模不准确,调度结果就会出现MPS瞬移这种荒唐解。我见过不少初版代码的MPS轨迹图看着像传送术,就是因为少了移动时间约束。
第二,配电网“拓扑重构”和MPS调度“并行决策”的协调。故障后配电网往往要通过联络开关操作恢复部分供电,开关状态一变,潮流路径就变,MPS的最佳接入点也跟着变。所以动态调度的模型不能只盯着MPS本身,必须把网络重构变量一起优化,这是一个典型的混合整数非线性的协同优化问题。
第三,MPS“电量有限”和负荷“时变性”的冲突。MPS容量就那么大,高峰期多放一点,后续时段可能撑不到检修完成;省着用又会让当前时段切负荷多。这个能量管理的本质是一个带约束的时序决策,等价于在每个MPS上跑一个微型的电池调度。
1.3 复现前需要具备的技术底子
如果你想跟着这篇博文完整走下来,我建议你先确认自己有这几样基本功,不然中途会很难受:
- Matlab基础操作熟练,至少看得懂脚本和函数的调用关系,能调试断点;
- 了解Yalmip工具箱的基本建模语法,知道
sdpvar、binvar、optimize这些基本命令的用法; - 有一定的配电网潮流知识储备,知道什么是DistFlow方程、什么是辐射状网络的拓扑约束,这是模型的核心;
- 有一个可用的优化求解器,Gurobi或者CPLEX都行,MILP规模一大,单纯用Matlab自带的
intlinprog会慢到怀疑人生。
如果你已经有上篇的代码基底,那最好,直接把下篇的动态调度模块挂上去即可。如果手头没有任何代码,那也不用慌,本文从模型到代码主框架全都有,可以一步步搭起来。
2. 动态调度模型拆解
2.1 目标函数:把“少切负荷”写成数学语言
动态调度的核心目标很朴素:让停电时间内的累计失负荷最小。但写进优化模型的时候,得把“负荷价值”也考虑进去。我复现的文献用的是带权重的切负荷最小化,权重可以按负荷类型分:一级负荷(医院、通信基站、应急指挥)权重高,二级、三级负荷权重依次降低。目标函数的形式大致是:
[ \min \sum_{t \in T} \sum_{i \in B} w_i \cdot P^{curt}_{i,t} \cdot \Delta t ]
其中 ( P^{curt}_{i,t} ) 是节点i在t时段的切负荷有功,( w_i ) 是该节点的负荷权重,( \Delta t ) 是时段长度。如果文献里还考虑了网络重构的操作成本(开关动作次数惩罚),我会在目标里再加一项线路开关状态变化的惩罚项,防止模型频繁操作开关。
有一点要提醒:如果故障持续时间长、MPS数量少,单纯最小化切负荷会出现“牺牲远处用户、保近处用户”的极端方案。所以有的论文会在目标函数里再加入一个最小切负荷的惩罚单位成本,或者用“最大最小”公平性约束。我复现的这版用的是加权单目标,做毕业设计或者课程项目完全够用,如果想发论文再考虑多目标。
2.2 决策变量:三类变量的职责与耦合
这套模型里的决策变量分成三大类,每一类的物理含义和数学性质都不同:
连续变量:
- 各节点各时段的有功、无功注入/消耗;
- MPS在接入节点的充放电功率 ( P^{mps}_{i,t} );
- 节点电压幅值的平方 ( U_{i,t} );
- 切负荷量 ( P^{curt}_{i,t} );
- MPS的电池电量状态 ( SOC_m(t) )。
二进制变量:
- 节点i在t时段是否由某台MPS接入供给 ( x_{m,i,t} );
- 线路开关状态 ( z_{ij,t} ),1表示闭合;
- MPS是否处于移动状态 ( y_{m,t} )。
辅助变量:
- 用于Big-M松弛的辅助连续变量;
- 用于辐射状拓扑约束的虚拟潮流变量。
这三类变量之间不是独立的,最经典的耦合就是“MPS接入”和“MPS移动”的互斥。我通常把它写成:
[ y_{m,t} + \sum_{i} x_{m,i,t} \le 1 \quad \forall m, t ]
意思是某一台MPS在同一个时段要么在移动,要么在某节点接入供电,二选一。这个约束看起来简单,但漏了它,求解出来的MPS轨迹就会“一路火花带闪电”地不合理。另外,SOC的更新也依赖接入状态,只有接入时充放电功率才不为0,这需要用另一个Big-M约束把功率变量和二进制接入变量关联起来。
2.3 约束体系:电源平衡、拓扑约束与MPS时空耦合
约束体系是整个模型的重头,我按四个层次来讲。
第一层是潮流约束。配电网一般是辐射状,用DistFlow方程可以精确描述。但DistFlow是二次约束,直接进MILP会加大求解难度,所以复现时几乎清一色用线性化DistFlow:将电压幅值平方项 ( U_i ) 作为变量,忽略网损高阶项,功率平衡和电压降落关系就变成线性等式/不等式:
[ P_{j,t} = P_{i,t} - P^{load}{j,t} + P^{mps}{j,t} + P^{curt}_{j,t} ]
二阶锥松弛(SOCP)是另一种常见做法,Gurobi对SOCP支持很好,精度也高。如果文献里明确用SOCP,我建议直接上,如果代码模板是线性化的,那就没必要去改成SOCP,徒增求解时间。
第二层是辐射状拓扑约束。这里有个容易被忽略的细节:故障后的网络重构,需要在每个时段都保证网络是辐射状(开环),不能出现环网。通常用“虚拟潮流法”,给每条线路定义虚拟流量,然后规定除根节点外每个节点只有一条上游线路作为“虚拟父线”,这样就能保证网络无环。写成约束是:
[ \sum_{j \in \Omega_i} f_{ji,t} - \sum_{k \in \Lambda_i} f_{ik,t} = 1 \quad \forall i, t ]
[ 0 \le f_{ik,t} \le M \cdot z_{ik,t} ]
简单来说,就是让虚拟流量只能从根节点流向负荷,任何环网都会导致虚拟流量无法满足节点平衡。这个约束加不加、加得对不对,直接决定解出来的拓扑是不是树,我调试的时候吃过不少亏。
第三层是MPS移动的时空耦合。MPS从一个节点撤出到另一个节点接入,中间至少需要行驶时间 ( t^{travel}_{m} )。这套约束在数学上是一个“移动禁止窗口”:
[ x_{m,i,t} + x_{m,j,t+\tau} \le 1 \quad \forall m, i, j \in \Omega_m, t ]
意思是如果MPS在t时刻在节点i供电,那在 ( t+\tau ) 之前,它不可能同时在节点j供电。这里的 ( \tau ) 至少要大等于两个节点间的行驶时间换算的时段数。要注意,这个约束的规模很大,MPS数量一多、节点数一多,组合爆炸的苗头立刻出现。我通常会在建模前先预处理一个“节点间最小行驶时段矩阵”,把所有可通行节点对的行驶时间提前算好,只给 ( \tau > 1 ) 的节点对建模,能省掉大量冗余约束。
第四层是电池SOC约束。MPS的电池模型照搬经典储能模型即可。充电效率、放电效率分开定义,SOC上下限、充放电功率上下限全部绑定。一个容易出问题的点是SOC的初始值:动态调度是承接预配置的,所以每台MPS的初始SOC就是预配置阶段给的容量状态,不能随便初始化。
2.4 场景不确定性处理:从确定性到鲁棒/随机
复现文献如果强调“预配置”和“动态调度”结合,那动态调度的具体实现通常有两条路线:
- 确定性路线:给定一组故障场景(比如台风导致哪几条线路断开),在每个场景下各自跑一个确定性的动态调度,最后统计期望切负荷量。这种方法简单直观,适合代码初版和教学复现。
- 滚动时域路线:每15分钟或半小时滚动调用一次优化,把当前时刻的实测状态作为初始条件,对未来的预测窗口做优化。这个更贴近实际工程,但代码复杂度明显高,需要额外处理“当前MPS在哪”的状态记录。
我复现的偏滚动时域确定性路线:每个调度时间窗重新计算一次,MPS的当前状态(位置、SOC)会从一个全局状态结构体读入。这样的好处是,如果后期想升级成两阶段鲁棒或者分布鲁棒,只需要改场景生成和主问题/子问题的迭代结构,外层框架不用大动。
3. Matlab代码实现核心主框架
3.1 从全局看代码结构:主程序、参数与子函数分层
如果你的工程能力和我一样普通,强烈建议把代码按“数据–建模–求解–结果”四层分离,别把一团代码堆在一个脚本里。我这里给出一个可直接照搬的目录结构:
MPS_Dynamic_Scheduling/ ├── main_dynamic_scheduling.m ├── data/ │ ├── load_params.m // 系统参数(节点、线路、负荷) │ ├── fault_scenarios.m // 故障场景(断线位置、持续时间) │ └── mps_params.m // MPS参数(数量、容量、初始SOC) ├── model/ │ ├── create_variables.m // 定义Yalmip变量 │ ├── add_constraints.m // 填充约束(含子函数) │ ├── add_objective.m // 目标函数 │ └── solve_model.m // 调用Gurobi求解 ├── utils/ │ ├── distflow_linear.m // 线性DistFlow约束生成 │ ├── topology_radial.m // 辐射状拓扑约束生成 │ └── preprocess_travel.m // 预处理MPS行驶时间矩阵 └── results/ ├── plot_mps_trajectory.m // 绘制MPS轨迹 ├── plot_load_restore.m // 绘制负荷恢复曲线 └── plot_voltage.m // 绘制电压分布主程序只负责四件事:加载参数、调用建模、求解、输出结果。所有细节都封装到子函数里,这样出错时排查路径清晰,不会出现“改了局部却不知道影响哪里”的尴尬。
3.2 DistFlow线性化与Big-M:别小看这两步
我用的线性化DistFlow约束,核心就三条等式。先定义变量节点电压平方 ( U_i )、支路有功 ( P_{ij,t} )、支路无功 ( Q_{ij,t} ),然后:
[ P_{ij,t} = \sum_{k: (i,k) \in E} P_{jk,t} + P^{load}{j,t} - P^{mps}{j,t} - P^{curt}_{j,t} ]
[ Q_{ij,t} = \sum_{k: (i,k) \in E} Q_{jk,t} + Q^{load}{j,t} - Q^{mps}{j,t} ]
[ U_i - U_j = 2(r_{ij} P_{ij,t} + x_{ij} Q_{ij,t}) / V_{base}^2 ]
第三个式子中 ( V_{base} ) 是基准电压,单位换算时一定要注意,否则就会出现电压降幅大得离谱的荒唐结果。Big-M的取值也是门学问。取太大,数值病态,求解器容易报数值警告;取太小,约束失效,把本来不满足条件的解放进来。我一般先算一下系统最大可能传输功率,把它乘以一个1.2的安全系数,作为M。每个约束都用独立的M变量,不要共用一个全局大M,效果会好很多。
3.3 Yalmip建模核心代码片段
Yalmip建模部分我给出一个骨架示例,方便你在自己的代码里快速对齐。假设节点数 ( N )、时段数 ( T )、MPS数量 ( M )。定义变量:
% 节点电压平方 U = sdpvar(N, T, 'full'); % 支路有功/无功流(有向,包含所有可能的辐射状支路) P_branch = sdpvar(n_branch, T, 'full'); Q_branch = sdpvar(n_branch, T, 'full'); % 切负荷量 P_curt = sdpvar(N, T, 'full'); % MPS 充放电功率(正为放电) P_mps = sdpvar(M, N, T, 'full'); % MPS 接入二元变量 x_mps = binvar(M, N, T, 'full'); % 线路开关状态 z_line = binvar(n_branch, T, 'full');加约束时,一条条用Constraints = [Constraints; ...]追加。这里我特别提醒一个问题:如果直接对三维变量P_mps和x_mps用sum函数,Yalmip的维度规则有时会引发不可预测的维度错误,建议用sum(P_mps, 2)这种显式指定维度求和,或者干脆循环节点和时段,代码虽然看着啰嗦,但出错率低,便于调试。
3.4 求解器配置与结果输出
我用的是Gurobi加Yalmip。Yalmip调用Gurobi的一般套路是:
options = sdpsettings('solver', 'gurobi', 'verbose', 2); options.gurobi.MIPGap = 0.01; options.gurobi.TimeLimit = 1800; sol = optimize(Constraints, Objective, options);如果跑的时间太长,可以把MIPGap放松到0.05,工期不紧的研究课题完全能接受。求解完成后,value()把变量值取出来,直接可视化。
需要注意,Gurobi许可证分学术和商用,学术用户申请站内许可证很顺利。如果你临时没有Gurobi,可以用沪上另一款求解器的免费学术版,或者先用Matlab自带intlinprog验证小算例,确认模型无误后再换商用求解器提速。CPLEX也可以,但配置就在Yalmip里把solver改成cplex就行,代码其余部分不用改动。
4. 实操运行记录与参数调整心得
4.1 数据准备与算例初始化流程
数据输入这部分要特别细心。我用的是修改版的IEEE 33节点系统:33个节点、32条支路、5条联络开关支路,基准电压12.66kV。故障场景设置两条线路因台风断开,一台MPS容量500kWh,初始SOC 90%,允许放电功率100kW。
复现的时候,如果手头没有现成的负荷数据,可以用Matlab生成一组24小时的分段常数负荷曲线(每4小时一个阶梯),这个数据精度对验证模型已经足够。关键是确保节点负荷的总量级没有离谱到线路容量超限,否则模型会一直不可行,让你误以为约束写错了。
4.2 从运行到结果判读:看哪些指标
跑完主程序后,我会做三个检查:
第一,看MPS的轨迹图。确认MPS没有瞬移,每条“撤出—转移—接入”路径的时长都大于等于预处理算出来的行驶时间。第二,看负荷恢复曲线。正常情况下,优先级高的负荷会在故障后快速恢复,恢复比例随MPS到达时间阶梯上升。第三,看电压分布。MPS接入点的电压不应该明显越限,如果电压偏低,可能是无功支撑不够,需要检查是不是只建了有功调度模型而无功没跟上。
输出结果时,我习惯把三张图保存成高分辨率图片,放在结果文件夹里。编写plot_mps_trajectory.m时,X轴是时段,Y轴是节点编号,每台MPS用一条彩色线段串起来,线段上标注“接入/移动/撤出”状态,这张图是论文和答辩的必备素材。
4.3 几个敏感参数的调节方法
动态调度模型里,我对这几个参数最敏感,调一次会让结果明显变化:
MIPGap:放松到0.05,求解时间可能缩短到原来的十分之一,代价是切负荷成本目标值有微小偏差,但对方案演示基本无影响。- 移动时间矩阵:如果节点之间的距离矩阵计算错误,MPS在时域上的轨迹会受阻,严重时直接不可行。建议先用一个循环打印“MPS 1可以到达的节点及其行驶时段”,肉眼核对一遍。
- 负荷权重:把一级负荷权重调到10:1:0.1,会明显看到MPS优先往医院、通信站所在节点跑。如果调了半天MPS还是乱跑,去检查权重是不是被归一化了。
5. 复现中踩过的坑与排查技巧
5.1 一直报“Infeasible problem”的三个排除步骤
最让我头疼的问题就是求解器返回不可行。规范的做法是:先检查节点功率平衡约束。把MPS接入二元变量基础上加一个小ε(比如乘上1e-6),人为给模型制造一点点松弛空间,如果立刻变可行,说明原模型里某个节点的功率平衡约束太紧,通常是因为负荷参数太大而MPS容量不够。
第二步检查辐射状拓扑约束。把拓扑约束注释掉重新求解,如果变成可行,问题就出在虚拟潮流约束上,多半是根节点的虚拟源设置不对。第三步检查MPS移动互斥约束。把“MPS接入-移动互斥”放开,如果可行,说明你给的行驶时间窗口太小,MPS在路段切换上没来得及。这一步很耗时间,但每排除一个模块,就确认了一部分建模的正确性,值得做。
5.2 求解器常报数值警告或者卡住的排查路径
跑Gurobi时常遇到“Numerical problems”警告。我总结下来主要是两类原因:
- Big-M太大,导致约束矩阵尺度差异过大。解决思路是对每个约束单独设M,能收敛的尽量收敛;
- 电压平方 ( U ) 的初始范围设得太大。把
sdpsettings('gurobi.NumericFocus', 3)打开,能缓解数值问题,但根本还是要回头检查参数量纲。
卡住的排查路径,一般是整数变量太多了。33节点、24时段、3台MPS,粗略算一下就有 ( 33 \times 24 \times 3 ) 个二进制接入变量,再算上开关状态,规模达到上千个二元变量,这对MILP来说已经是硬仗了。给求解器加TimeLimit是常态,如果一定要追求最优解,就减少时段数量,比如把1小时一个时段改成2小时一个时段,时段数少一半,求解时间能下降一个数量级。
5.3 结果异常:MPS轨迹乱飞是建模问题还是参数问题?
有段时间我的MPS轨迹出现“割裂式跳跃”,比如同一台MPS在t=1时在节点5供电,t=2时跳到节点20供电,且中间没有移动过程。排查思路是先去检查“接入-移动互斥”约束有没有生效。如果约束加在Constraints但也因为索引不对而实际没加进去,那我遇到过——Yalmip对变量是全模型可见的,但约束漏加了根本不会报错,只有看结果时才发现。
另一个原因是对非线性约束用了implies()函数,Yalmip的implies会自动生成Big-M表达式,但这个M有时很大,数值上容易把约束松弛掉。解决方法是手动写等价线性约束,比如:
[ P^{mps}{m,i,t} \le P^{max} \cdot x{m,i,t} ]
这样数值上更稳定,含义也更清楚。
6. 个人体会与后续可扩展方向
这套复现做下来,我个人最大的感受是:比起预配置那种偏“组合爆炸”的选址问题,动态调度的难点不在于变量多,而在于模型中的逻辑关系多。MPS的移动、接入、充电放电、网络开关状态,一环扣一环,任何一个逻辑约束遗漏或误写,极难靠看数值去定位问题。所以复现这个项目的策略一定是:先把一个简化算例(3节点+1台MPS+2个时段)调到完全正确,再逐步扩大算例规模。直接从33节点全时段开始调,大概率会在各种错误中迷失方向。
如果后续你还想进一步扩展,我建议走这三个方向:
第一,把确定性调度升级为两阶段分布鲁棒,让负荷预测的不确定性显式进入约束,这正好能同“上篇”的预配置框架完美衔接。第二,在动态调度中加入维修队调度。现实中,故障线路的维修进度直接影响停电时长,MPS调度和维修队调度联合优化,会是一个更贴近工程实际的创新点。第三,把MPS调度和储能系统、柴油发电机等异构应急资源统一建模。异构资源的时空耦合约束更复杂,但调度效果的提升也是明显的。
最后给个建议:如果你打算做相关毕设或发小论文,不要只停留在跑通代码。试着把故障场景随机生成一组(比如50个),跑蒙特卡洛模拟,统计MPS动态调度的负荷恢复率提升效果。这个统计结果如果在5%以上,并且配几张MPS轨迹图和负荷恢复曲线图,无论放答辩PPT还是论文里,都是很有说服力的支撑材料。
如果各位在实际复现时遇到新的报错、或者发现我的代码逻辑和文献细节对不上,欢迎一起交流讨论。这类配电网韧性的题目本来就是“一通则百通”,把MPS动态调度啃下来,后续再上手异构资源调度、配网重构、储能配置都会顺畅很多。