复现完上篇的MPS预配置模型之后,我一直觉得动态调度才是最磨人的部分。预配置好歹是“一次性决策”,把移动电源放到哪儿,优化器算一次就完了;动态调度要把时间维度和空间维度一起揉进去,每台移动电源每个时段在哪个节点、带多少负荷、要不要挪窝、挪窝路上算不算可用,这些约束全部耦合在一起,建模的时候稍不留神就是几十个二进制变量白送。这篇就专门把下篇的动态调度部分掰开揉碎,从数学建模到Matlab代码架构,再到我实际跑算例时踩过的几个坑,一次性说清楚。
整个复现用的测试系统是某个标准的33节点配电网,手上有3台应急移动电源(MPS),灾害场景预设了两条馈线故障,负荷曲线取的是某地区夏季典型日数据。文章面向的是正在复现韧性提升类论文、或者在配电网灾害应急方向做研究的同学,尤其适合已经搞定预配置、但对“移动电源动态调度”这部分怎么建模、怎么写代码还不太有底的人。
1. MPS动态调度问题的定位:从预配置到实时响应
1.1 两阶段框架里动态调度到底解决什么问题
先捋清楚这个问题的位置。上篇的预配置解决的是“灾害来之前,移动电源应该停在哪里”。它站在日前视角,用预测的灾害信息(比如台风路径、线路损坏概率)去做决策,目标是在灾害发生后能尽快让MPS投入恢复供电。
动态调度则完全不同。它站在实时视角,假设灾害已经发生,某些线路已经断了,此时你手里有数量有限的MPS,每个MPS都是一个可移动的储能单元。你现在要回答的是:这些MPS在接下来的24小时(或者72小时)里,每个时刻应该接到哪个节点、输出多少功率、什么时候应该转移位置、转移到哪里。
这里有一个非常关键的工程背景:配电网在极端事件后的恢复过程是动态的。抢修队修复线路需要时间,负荷也会随居民用电习惯实时变化,一台MPS如果死守在一个节点,虽然能保住那一片负荷,但可能白白浪费了它移动的能力。而如果频繁移动,又会产生转移时间成本——在移动途中它是完全不可用的。动态调度要做的就是在“保负荷”和“机动性”之间找一个最优折中。
我复现的时候最大的感触是:动态调度本质上是一个多时段混合整数线性规划问题,但它的难点不在于模型复杂度本身,而在于如何把“移动”这件事用数学约束准确写出来。移动不是瞬时的,需要时间;移动也不是无条件的,路网可能因为灾害受损,两点之间未必可达。这些细节才是论文里寥寥几页、但代码里极其折磨人的部分。
1.2 动态调度与预配置的衔接逻辑
两个阶段是怎么衔接的,这也是很多初学者容易懵的地方。上篇预配置的输出是“MPS初始停靠位置”,这个结果在下篇里直接作为动态调度的初始条件使用。也就是说,动态调度模型里MPS在第1时段的初始位置是一个已知参数,不是决策变量。
这个衔接逻辑我建议务必在代码里用参数结构体显式传递,别手动填数。我当时是先跑预配置脚本,把结果存成.mat文件,然后动态调度脚本里用load读进来。这样两个模型的耦合关系一目了然,后续换灾害场景、换负荷数据,只要改输入文件,两个阶段自动联动,省了非常多重复劳动。
另外一个衔接点是时间粒度。预配置通常以“小时”为粒度就够用了,但是动态调度里MPS的移动时间往往只有几十分钟,如果你的时间步长太大,移动时长的建模精度会大打折扣。我实测下来,24小时仿真用1小时间隔可以接受,但如果你把仿真周期拉到72小时,务必考虑时间步长对求解规模的影响——后面第4章会详细讲这个坑。
2. 数学模型:把“移动电源怎么跑”写成约束
2.1 目标函数:损失最小化的正确打开方式
先说目标函数。绝大多数韧性提升类论文的目标函数都围绕“负荷损失最小化”展开,但具体写法有讲究。我复现的那篇原文用的是加权负荷损失之和最小化,权重按照节点负荷的重要程度设置。医院、应急指挥中心这类一级负荷权重拉满,普通居民负荷权重低一些。
目标函数的数学形式可以写成:
[ \min \sum_{t \in T} \sum_{i \in N} \omega_i \cdot L_i \cdot (1 - x_{i,t}) \cdot \Delta t ]
其中 (x_{i,t}) 是节点 (i) 在时段 (t) 的供电状态(0-1变量),(L_i) 是该节点的负荷功率,(\omega_i) 是权重系数,(\Delta t) 是时间步长。
这里有个细节容易被忽略:目标函数里要不要加MPS的移动成本项。我仔细对比过原文的写法和其他几篇同方向论文,发现大部分模型不考虑移动的“经济成本”,只考虑移动带来的“时间成本”——因为MPS是应急资产,移动的燃油/电量消耗与负荷损失相比量级太小,加了反而干扰主目标。但移动导致的不可用时段,已经通过约束间接反映在目标函数里了。所以最终我只保留了负荷损失这一项目标,简洁且够用。
2.2 移动路径建模:路网、时序与可达性约束
这部分是动态调度建模的核心难点,值得花篇幅展开。
MPS的移动路径不是简单的“从A点到B点拉一条直线”,而是要走实际路网。灾害后路网也可能受损,某些路段无法通行。论文里通常用“可达性矩阵”来处理:预计算任意两个候选节点之间在路网上的最短通行时间,如果不可达则设为无穷大。
在实际建模中,我引入了一个0-1变量 (m_{j,t}) 表示MPS (j) 在时段 (t) 是否处于移动状态。核心约束是:移动状态下不能向电网注入功率,且移动需要消耗一定的时间。
[ p^{MPS}{j,t} \le M \cdot (1 - m{j,t}) ]
[ \sum_{k=t}^{t+\tau_{ij}-1} m_{j,k} \ge \tau_{ij} \cdot (y_{j,i,t} + y_{j,i,t+\tau_{ij}} - 1) ]
第二条约束稍微解释一下:如果某台MPS在时段 (t) 从节点 (i) 出发,经过 (\tau_{ij}) 个时段后到达节点 (j'),那么在这中间的每一个时段它都必须处于移动状态。这个约束是我调试时间最长的地方,因为(\tau) 是整数,这里涉及索引越界的处理,后面会提到代码实现技巧。
还有一个容易犯的错:MPS到达新节点后,必须满足“至少停留若干时段才能再次移动”的约束。这个在原文里没有显式写,但我实际跑算例的时候发现,如果不加这个最小停留时间约束,优化器会给出一种极端解——MPS每个时段都在不同的节点之间跳,虽然目标函数值很好看,但工程上完全不可行。后来我在模型里加入了最小停留时段约束,解的质量立刻变得合理多了。
2.3 接入点与功率注入约束:不能一台MPS同时出现在两个节点
这个约束看起来简单,却是最容易出bug的地方。一台MPS在任意一个时刻只能接入一个节点,用约束表达就是:
[ \sum_{i \in N} y_{j,i,t} = 1, \quad \forall j \in J, t \in T ]
其中 (y_{j,i,t}) 是0-1变量,表示MPS (j) 在时段 (t) 是否接入节点 (i)。
但要注意,这个约束和移动状态变量 (m_{j,t}) 是联动的。如果MPS在移动中,它虽然“在某条路上”,但不接入任何电网节点。所以更准确的表达是:
[ \sum_{i \in N} y_{j,i,t} = 1 - m_{j,t} ]
也就是说,MPS要么接入某个节点正常供电,要么处于移动状态。这个表达才是完整的。
功率注入约束方面,MPS接入节点后的输出功率要满足上下限:
[ P^{min}{j} \cdot y{j,i,t} \le p^{MPS}{j,t} \le P^{max}{j} \cdot y_{j,i,t} ]
很多论文还会同时约束SOC(荷电状态)的动态变化:
[ SOC_{j,t+1} = SOC_{j,t} - p^{MPS}{j,t} \cdot \Delta t / E^{cap}{j} ]
[ SOC^{min}{j} \le SOC{j,t} \le SOC^{max}_{j} ]
这里我建议给SOC建模的时候保留一点裕度,不要把SOC下界设成0。实际MPS的电池保护策略通常不允许深度放电,我复现的时候设的是0.1到0.9,既符合工程实际,又能防止模型因为边界太紧而出现不可行。
2.4 潮流约束与配电网重构的耦合
MPS接入电网后,它的功率注入会改变节点电压和支路潮流,所以不能只建MPS自身的约束,还要把配电网潮流方程耦合进来。
我复现的论文用的是DistFlow线性化潮流模型。相比完整交流潮流,DistFlow在辐射状配电网精度足够,而且线性化之后能保持整个模型是MILP,可以直接用商业求解器求解。
DistFlow的核心方程长这样:
[ P_{i,t} = \sum_{k \in child(i)} P_{k,t} + p^{load}{i,t} - p^{MPS}{i,t} ]
[ Q_{i,t} = \sum_{k \in child(i)} Q_{k,t} + q^{load}_{i,t} ]
[ V_{i,t} = V_{parent(i),t} - \frac{r_{ik}P_{k,t} + x_{ik}Q_{k,t}}{V_0} ]
其中电压幅值约束:
[ V^{min} \le V_{i,t} \le V^{max} ]
这里额外说一句:如果你对配电网潮流建模比较熟,可以考虑用二阶锥松弛(SOCP)替代完全线性化,这样能保留更多非线性信息,但对求解器要求更高,Gurobi处理SOCP也没问题。我当时两个方案都试过,在33节点系统上,SOCP版本的求解时间比MILP版本多了约40%,但结果差异不到1%。如果你的算例规模更大,建议先用线性化版本跑通流程,再视精度需求决定要不要升级。
3. Matlab实现架构与关键代码
3.1 数据组织:节点、线路、灾害场景、路网四张表
代码写得好不好,数据组织占一半。我复现的时候把所有输入数据统一封装成结构体,文件放在一个Data目录下,每个数据文件对应一个脚本,用脚本生成.mat文件统一加载。好处是后续换算例只用改数据脚本,模型代码一行不用动。
我整理的数据结构是这样的:
% 节点参数 bus.id = (1:33)'; bus.p_load = loaddata; % 33x24 每节点每小时的负荷 bus.q_load = loaddata * 0.3; % 无功按0.3系数折算 bus.vmin = 0.95; bus.vmax = 1.05; bus.weight = ones(33,1); % 负荷权重,重要节点调高 % 线路参数 line.from = [1 2 3 ...]'; % 首端节点 line.to = [2 3 4 ...]'; % 末端节点 line.r = [0.0922 0.4930 ...]'; % 电阻 line.x = [0.0470 0.2511 ...]'; % 电抗 line.fail = zeros(32,1); % 故障标记,1表示断开 line.fail([7 19]) = 1; % 设定两条馈线故障 % 灾害场景 disaster.horizon = 24; % 仿真时段数 disaster.dt = 1; % 单位:小时 % 路网可达时间 % road.time(i,j) 表示从节点i到节点j的通行时间(小时) road.time = roadtime_matrix;这个数据结构的好处是你可以在Data脚本里用可视化函数先检查一遍数据,比如画出配电网拓扑、负荷曲线,确认无误后再跑优化,极大减少“优化结果异常但其实是数据错了”的排查时间。
3.2 YALMIP+Gurobi的建模模板
求解器我选的是YALMIP+Gurobi的组合。YALMIP负责建模,Gurobi负责求解。选这个组合的原因是Gurobi对MILP的求解性能确实能打,而且YALMIP的语法简洁,调试方便。
核心建模代码骨架如下:
% 定义决策变量 y = binvar(nMPS, nBus, T, 'full'); % MPS接入状态 m = binvar(nMPS, T, 'full'); % MPS移动状态 p_mps = sdpvar(nMPS, nBus, T, 'full'); % MPS注入功率 soc = sdpvar(nMPS, T+1, 'full'); % 荷电状态 v = sdpvar(nBus, T, 'full'); % 节点电压 p_br = sdpvar(nLine, T, 'full'); % 支路有功 q_br = sdpvar(nLine, T, 'full'); % 支路无功 x_state = binvar(nBus, T, 'full'); % 节点供电状态 % 目标函数 objective = 0; for t = 1:T for i = 1:nBus objective = objective + bus.weight(i) * bus.p_load(i,t) * (1 - x_state(i,t)) * disaster.dt; end end % 约束条件 Constraints = []; % 1. MPS接入/移动唯一性约束 for j = 1:nMPS for t = 1:T Constraints = [Constraints, sum(y(j,:,t)) == 1 - m(j,t)]; end end % 2. MPS功率上下限约束 for j = 1:nMPS for i = 1:nBus for t = 1:T Constraints = [Constraints, ... 0 <= p_mps(j,i,t) <= Pmax(j) * y(j,i,t)]; end end end % 3. SOC动态约束 for j = 1:nMPS for t = 1:T Constraints = [Constraints, ... soc(j,t+1) == soc(j,t) - sum(p_mps(j,:,t)) * disaster.dt / Ecap(j)]; Constraints = [Constraints, ... soc_min(j) <= soc(j,t+1) <= soc_max(j)]; end end % 4. DistFlow潮流约束(简化展示) for t = 1:T [Constraints, ~] = build_distflow(Constraints, bus, line, ... p_br(:,t), q_br(:,t), v(:,t), ... p_mps(:,:,t), x_state(:,t)); end % 5. 移动时间约束(核心) Constraints = add_mobility_constraints(Constraints, y, m, road, nMPS, nBus, T); % 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 2, ... 'gurobi.MIPGap', 0.01, 'gurobi.TimeLimit', 7200); optimize(Constraints, objective, ops);这里有几个细节要注意:
第一,build_distflow和add_mobility_constraints我非常推荐写成独立的函数。原因很简单,这两个约束块是全文最容易出错、也最需要反复调整的部分,独立成函数后,你可以针对性地做单元测试。我就是在写add_mobility_constraints的时候,单独构造了一个只有3节点、2台MPS的迷你算例来验证约束逻辑,确认无误后再接入全模型。
第二,sdpsettings里的MIPGap建议设置得宽松一些,比如1%。动态调度模型在33节点系统上通常有几千个二进制变量,如果强求0.01%的MIPGap,求解时间会爆炸。实际工程决策里,1%的gap带来的目标函数偏差可能只有几十千瓦时负荷损失,但求解时间可能从20分钟降到2分钟,这个性价比非常高。
3.3 求解与结果处理:从解向量到动态策略表
优化的输出是一堆变量矩阵,不能直接用,必须处理成人类可读的“动态策略表”。我写了一个后处理脚本postprocess.m,核心是把 (y) 和 (m) 变量翻译成时间线。
% 生成MPS动态调度策略表 for j = 1:nMPS for t = 1:T [~, node_id] = find(y(j,:,t) == 1); if ~isempty(node_id) strategy{j,t} = sprintf('接入节点%d, 出力%.2f kW', ... node_id(1), value(p_mps(j,node_id(1),t))); else strategy{j,t} = '移动中'; end end end这个策略表生成之后,我会把它连同每个节点的电压曲线、负荷恢复率曲线一起画图。画图这一步别看简单,其实对验证结果合理性很有帮助。我记得第一次跑通的时候,发现某台MPS在凌晨时段跑到一个几乎没有负荷的节点去“旅游”了,就是靠节点负荷恢复率图看出来的。后来排查发现是路网时间矩阵里有一个节点的数据填错了,导致MPS从那个节点出发的时间被低估,优化器利用了这個漏洞。
另外,结果可视化建议用热力图展示MPS的位置随时间的变化,横轴是时间,纵轴是节点编号,颜色标注MPS编号或者功率大小。这种图放在论文里也非常直观,我当时还用它做了不同灾害场景的对比。
4. 复现中绕不开的坑与解法
4.1 二进制变量爆炸:时间粒度和节点聚合
动态调度的噩梦就是从二进制变量爆炸开始的。假设33个节点、3台MPS、24个时段,仅接入状态变量 (y) 就有 (3×33×24=2376) 个二进制变量,再加上移动状态变量 (m) 和节点供电状态 (x),总数轻松突破三千。
Gurobi对付几千个二进制变量其实还有余力,但你如果做72小时仿真、或者系统规模扩大到100+节点,求解时间会从分钟级跳到小时级,甚至直接内存溢出。
我实测对比了两个方案。方案一是把时间步长从1小时改成2小时,二进制变量数量直接减半,但代价是移动时间建模精度下降——如果某台MPS的转场时间是1.5小时,在2小时步长下它会是“移动2小时”而不是“移动1.5小时”。方案二是节点聚合,把电气距离接近、负荷类型相似的节点合并成一个“供电区域”,MPS只在区域层面移动,到了区域内部再选择具体接入节点。
最终我采用的是方案一和方案二的折中。具体来说,仿真前12小时(灾害刚发生,恢复动作最密集)用1小时间隔,后12小时用2小时间隔,变时间步长处理。这样既保证了关键时段的精度,又把二进制变量总量压了下来。实现上,只要在约束里把时间索引从t映射到实际时刻即可,YALMIP的索引灵活性完全可以处理。
4.2 SOCP松弛在径向网络中的有效性
前面提到潮流模型可以选线性化或者SOCP。这部分我多说几句。
DistFlow的线性化版本把电压降方程里的二次项丢掉了,在支路电流较大的时候会有一定误差。但配电网在应急恢复场景下,支路功率通常不会达到满载极限,所以线性化误差通常在5%以内。如果你复现的论文审稿人较真,要求在模型描述里体现潮流精度,建议用SOCP版本。
SOCP版本的电压降约束长这样:
[ | \begin{bmatrix} 2P_{ik,t} \ 2Q_{ik,t} \ U_{i,t} - U_{k,t} \end{bmatrix} |2 \le U{i,t} + U_{k,t} ]
在YALMIP里直接写norm约束就行,Gurobi会把它识别为二阶锥约束。我两次都跑过,结论是:SOCP版本在33节点系统上求解时间约是线性化版本的1.4倍,最优目标值差异不到1%。所以我最终交付的代码保留了SOCP版本作为“高精度模式”,默认跑线性化版本。
这里有一个很重要的调试心得:不要用整个33节点系统去验证潮流模型对不对。我先用IEEE 4节点系统做单元测试,手动手算一遍DistFlow结果,然后对照代码输出,确认无误后再扩大到全系统。否则一旦潮流错了,目标函数异常,你根本不知道是潮流的问题还是MPS约束的问题。
4.3 冷负荷启动与MPS容量约束的时间耦合
这个坑是审稿人最喜欢问、复现者最容易被问倒的点。
电力系统里有个物理常识:长时间停电后,空调、冰箱这类负荷重新上电的瞬间电流远大于稳态电流。文献上叫“冷负荷启动”(cold load pickup)。你在动态调度模型里如果直接用稳态负荷 (L_i) 算恢复量,那么在停电几个小时后重新供电的那一瞬间,实际需要的功率可能是稳态的3到5倍。
我复现的原文里没有显式建模冷负荷启动,但我在和目标对比的时候发现,如果完全不考虑这个特性,MPS的功率上限约束会严重失真——模型以为自己能带起500kW的负荷,实际上现场一合闸直接过载。
我的处理办法是:在负荷恢复状态变量x_state从0变成1的那一瞬间,增加一个额外的“启动功率”约束:
[ p^{MPS}{i,t} \ge CLF_i \cdot L_i \cdot \max(0, x{i,t} - x_{i,t-1}) ]
其中CLF是冷负荷因子,也就是启动电流倍数。这个约束虽然只影响每一个节点恢复瞬间一个时段,但它对MPS容量配置和调度策略的影响是全局性的——因为MPS必须在“带冷启动负荷”和“维持稳态负荷”之间取舍,这会直接改变最优的移动路径。
加了冷负荷启动约束之后,我复现的结果与原文图表拟合度明显提高。这也算是一个经验分享:单纯复现论文的时候,如果发现结果和原文对不上,先别怀疑代码bug,考虑一下原文是不是省略了这个物理细节。
4.4 求解器的参数调优与热启动
最后一个坑可能很多人不会注意到:Gurobi在MILP求解中,默认参数其实不是最优的。我复现时在sdpsettings里做了这样几个调整,效果非常显著:
gurobi.MIPFocus设置成1,让Gurobi把更多精力放在寻找可行解上。动态调度模型的高质量可行解比最优解的下界更有工程价值,因为边际收益递减。
gurobi.Heuristics设置成0.15。启发式搜索的比例默认是0,调高后能在短时间内找到多个可行解,配合MIPFocus效果很好。
gurobi.Threads设置成8或者16。这个看机器配置,我用的机器是8核16线程,设置成16后求解速度约提升了30%——多核机器千万不要浪费线程。
还有一个实用技巧是热启动(warm start)。上一时段求出的解,通常和下一时段的最优解不会差太远。我写了一个循环,每求解完一个时段,就把当前的y和m值作为下一时段的初始解传入:
assign(y, value(y)); assign(m, value(m)); ops.gurobi.StartNodeLimit = 500;实测下来,热启动可以把总求解时间缩短15%左右。别看比例不大,动态调度模型经常一跑就是几个小时,15%就是几十分钟的差别。
5. 实例演示:33节点系统上的调度结果解读
5.1 场景设置与参数说明
我的复现算例配置是这样的:33节点标准系统,节点7和节点19所在的两条馈线故障,故障持续时间设定为灾害发生后8小时,抢修队在时段t=9开始修复,每条线路修复耗时4小时。3台MPS,容量统一设定为2MWh,最大输出功率500kW,SOC工作范围0.1到0.9,初始SOC全部为0.85。
负荷数据用了夏季典型日的曲线,峰值负荷出现在下午3点左右(对应时段16),低谷在凌晨4点(对应时段5)。一级负荷节点(权重设为10)包括节点8和节点24,代表医院和应急指挥中心,其他节点权重为1。
路网时间矩阵按节点间的道路距离估算,通行时间从0.5小时到2小时不等。其中节点5到节点6的路段因为灾害导致路面损毁,通行时间从正常的0.5小时延长到3小时——这个细节是我故意加进去的,就是为了测试模型能否在处理时间变化的路网条件下做出合理决策。
5.2 调度结果解析:MPS的行动逻辑
求解完成后,我把3台MPS的行动计划列了出来。这里摘取几个有代表性的时段。
MPS1初始位置在节点6(这是预配置阶段算出来的最优位置)。故障发生后,它立刻从节点6接入电网,为节点7供电——节点7正是故障线路的下游节点,属于“被孤岛隔离但不含故障点”的失电区域。从时段1到时段8,MPS1全程稳定输出480kW,给节点7和节点8(医院)供电,期间没有移动,这个决策非常符合直觉:一级负荷权重高,值得让一台MPS死守。
MPS2初始位置在节点20,但它在时段4就启动了转移动作,目标节点是24。原因也简单:节点24是另一个一级负荷节点,虽然它的上级线路尚未故障,但电压已经跌到0.93p.u.,接近电压约束下界。MPS2在时段6接入节点24后,输出300kW,电压恢复到0.97p.u.。这个决策体现了动态调度的价值——它不只在故障线路下游保负荷,还兼顾了电压支撑功能。
MPS3的路径最有意思。它从节点10出发,先在节点11带了4个小时的负荷,然后在时段9——也就是抢修队开始修复线路的同一时段——向下游移动,在时段12接入节点12,为即将恢复供电的区域提供电压支撑。这个协同动作说明模型很好地捕捉到了“抢修恢复时序”与“MPS调度时序”之间的耦合:MPS提前移动到即将恢复的区域,既不影响当前区域的保供电(因为节点11的负荷可以暂时由主网恢复供电),又能为后续恢复过程提供支撑。
5.3 与“固定部署”策略的对比
为了验证动态调度的价值,我还跑了一个对照组:把3台MPS的位置固定在上篇预配置的位置,不允许移动,其他参数完全一致。两组结果对比如下:
| 指标 | 动态调度 | 固定部署 | 提升幅度 |
|---|---|---|---|
| 总负荷损失(MWh) | 18.6 | 24.3 | 23.4% |
| 一级负荷损失(MWh) | 0.8 | 1.6 | 50.0% |
| 最低节点电压(p.u.) | 0.947 | 0.931 | - |
| 平均恢复时间(h) | 3.2 | 4.7 | 31.9% |
这个对比结果非常直观地说明了动态调度的意义。固定部署策略下,MPS2死守在节点20,但节点20的负荷在白天并不高,能源利用率低下;节点24的电压问题得不到支撑。动态调度让MPS2从节点20转移到节点24,用一个“移动”动作同时解决了负荷损失和电压质量问题。
这里也回应一下很多人对动态调度的怀疑:论文里的花哨方法到底有没有用?从我自己复现的对比来看,类似的提升幅度是在合理的工程预期范围内的。动态调度没有让MPS多出力,只是让同样的MPS出现在更该出现的地方——这就是机动性的价值。
6. 复现代码架构与扩展思路
6.1 完整目录结构与模块划分
我的复现工程文件结构如下,供大家参考:
MPS_DynamicScheduling/ ├── Data/ │ ├── case33.m % 系统参数 │ ├── load_profile.m % 负荷曲线 │ ├── disaster_scenario.m % 灾害场景 │ ├── road_network.m % 路网时间矩阵 │ └── generate_data.m % 汇总生成data.mat ├── Model/ │ ├── build_distflow.m % DistFlow潮流约束 │ ├── add_mobility.m % 移动约束 │ ├── add_soc.m % SOC约束 │ └── objective_fn.m % 目标函数 ├── Solve/ │ ├── main_dynamic.m % 主程序 │ └── settings_gurobi.m % 求解器配置 ├── Postprocess/ │ ├── strategy_table.m % 生成策略表 │ ├── plot_results.m % 可视化 │ └── compare_metrics.m % 指标对比 └── Results/ └── *.mat % 结果存档模块化最大的好处是可以单独调试每个模块。我在写add_mobility.m的时候,单独构造了一个迷你算例:3个节点、2台MPS、4个时段,然后手算一遍期望的约束边界,设置断点逐步检查。这个习惯帮我抓到了至少3个索引错误,如果没有单元测试直接跑全模型,这些错误会藏在几千行代码里极难定位。
6.2 后续扩展方向:从单时段到多阶段协同
把动态调度跑通之后,其实可以沿着几个方向做扩展,都是论文发表或者实际项目里比较有价值的内容。
第一个方向是“预防-应急-恢复”三阶段协同。我复现的论文只有预配置和动态调度两阶段,但实际工程中还有“系统恢复阶段”——主网恢复供电后,MPS如何有序退出、如何与分布式电源协同。三阶段耦合建模的研究还不多,但工程意义很明确。
第二个方向是MPS交通路网与配电网的联合建模。我目前用的路网时间矩阵是静态的,但实际灾害中道路通行时间会随时间变化(比如上午清理完一段路,下午才能通行)。把路网恢复时序也建模进来,MPS的路径决策会更贴近实际。代价是模型复杂度显著上升,需要引入时间-空间网络流约束。
第三个方向是和强化学习结合。动态调度本质上是一个序贯决策问题,尤其当灾害场景不确定性强、需要快速滚动决策的时候,基于模型预测控制的框架会更高效。我目前在尝试把Gurobi求解出的最优策略作为专家数据,训练一个策略网络做实时决策逼近,效果还在验证中。
这些扩展方向的代码基础,都建立在当前这个动态调度MILP框架之上。把这一篇的基础模型和代码吃透,往上叠扩展只是工作量问题,而不是思路问题。
我最后想说一个复现过程中的真实体会:写代码的时间其实只占三成,剩下七成全在“验证代码的结果是否合理”。每次跑完一组算例,我会习惯性问自己三个问题:MPS的移动路径是否违反了时间约束?电压曲线是否在安全范围内?目标函数值的变化趋势是否符合物理直觉?三个问题都回答“是而且合理”,这一组结果才敢拿出来用。希望大家复现的时候也能保留这份职业怀疑精神——不管是论文的结论,还是自己的代码,都值得多问一句“这合理吗”。