智能微电网调度:MILP约束建模与MATLAB可执行性排查 简介面向电力系统、微电网调度及MATLAB仿真学习者这份智能微电网调度算法压缩包围绕多能源互补下的功率优化分配展开可用于理解光伏、风机、微燃机、储能与负荷模型的搭建以及目标函数、功率平衡和设备运行约束的建模过程帮助读者把调度问题从概念落实到可运行的算法流程。包内共20个文件以18个m脚本为主体辅以1个asv备份和1个xls数据表整体约19KB体量轻便却覆盖模型定义、智能优化求解、穷举对照与结果绘图等环节。已有530人学习下载适合课程设计、毕业课题或算法复现时参考。读者可从中梳理粒子群等优化方法在微电网调度中的实现思路结合数据表与绘图脚本观察不同策略的运行结果并借助穷举方案进行性能对照进而理解运行成本、碳排放与供电稳定性之间的权衡。1. 智能微电网调度为什么「最省钱的方案」往往执行不了把分时电价曲线和负荷曲线叠在一起多数人的第一反应是谷时充满、峰时放光。但真正跑过智能微电网调度的人在 MATLAB 里看到的解常常是另一副样子储能偏偏在电价并不最低的时段充电光伏还有一部分被主动弃掉。这不是求解器算错了而是目标函数只写了一半——另一半是机组爬坡、储能循环寿命、并网点倒送功率上限、日末荷电状态这些硬约束它们在互相拉扯。把问题写成混合整数线性规划之后才会发现经济性和可执行性之间隔着一堵墙。搞懂这堵墙是怎么砌起来的比记住几个算法名字有用得多。2. 调度模型的数学骨架目标函数从来不是最难的部分写调度的第一反应通常是「成本最小化」这五个字可真把公式落到代码里八成的时间花在约束上。目标函数无非是燃料成本、购售电差价、储能退化折算、弃负荷惩罚这几项加权写错了顶多结果偏一点约束写错了求解器会给你一个漂亮的「最优解」然后现场跑不起来。这就是调度问题最反直觉的地方它的难度不在优化而在描述。2.1 决策变量功率、荷电状态与启停状态三类一个典型的小型微电网一天 24 个时段决策变量大概分三族。第一族是连续功率变量可控机组各时段的出力、储能充电功率、储能放电功率、与外部电网的购电和售电功率、弃光量、可中断负荷的削减量。第二族是状态量储能的荷电状态 SOC它其实是约束的副产品但常常被写成显式变量便于加终端约束。第三族是 0-1 变量机组启停状态、储能充放电互斥标志。这里有个几乎所有人第一次写都会踩的简化把「购电」和「售电」合并成一个可以为负的变量 P_grid。数学上没错物理上错得离谱——因为一旦允许 P_grid 取负电价差就会诱导模型在同一时刻既买又卖赚取一个不存在的差价。常见的实现思路是把净功率拆成两个非负变量 P_buy 和 P_sell再补一条互斥约束。代价是多一组 0-1 变量规模上去了但模型变得可解释、可执行。另一处容易含糊的是时间步长。日前调度通常取 1 小时一天 24 个时段如果要做更精细的功率跟踪会取 15 分钟一天 96 个点。步长一变所有涉及功率的约束爬坡、SOC 递推的系数都要跟着变而这一步正是后面第 5 章反复翻车的地方。2.2 约束为什么比目标函数更难写功率平衡、爬坡与最小启停时间功率平衡是唯一一条「必须严格成立」的等式约束写起来却最容易出单位错误。形式很简单所有供给等于所有需求。供给侧是机组出力、光伏风电实际出力预测值减弃光、储能放电、购电需求侧是负荷、储能充电、售电、可削减负荷的剩余部分。问题在于每一项的单位到底是 kW 还是 kWh取决于你有没有乘时间步长。储能放电 P_dis 乘上 Δt 才是电量机组出力乘 Δt 才是燃料消耗对应的能量而负荷预测通常直接给的是 kWh/时段。单位混着写模型不会报错只会给你一个偏差 3% 到 5% 的解肉眼根本看不出来。机组出力上下限要跟启停状态耦合。写法是把出力夹在「停机时为 0、开机时为最小技术出力」之间P_min × u(t) ≤ P(t) ≤ P_max × u(t)其中 u(t) 是 0-1 的启停变量。这条约束的隐含意思是机组一旦启动就必须至少带到最小出力不能半死不活地运行。很多简化模型把 P_min 写成 0结果最优解里每台机组都只出一点点力看起来分得很平均实际上一台都开不起来。爬坡约束是第二道坎。机组在相邻时段之间的出力变化不能超过爬坡速率-R_down × Δt ≤ P(t) - P(t-1) ≤ R_up × Δt。麻烦在于机组启动瞬间的爬坡物理上并不适用如果模型不区分「已运行」和「刚启动」两种情形启动那一刻的功率跳变会被判为违反爬坡求解器只好被迫选择一种保守的运行方式经济性白白丢掉。常见的处理是引入启动/停机指示变量对爬坡约束按状态分段松绑代价是 0-1 变量数量再翻一倍。最小启停时间是约束里最「反优化」的一条机组一旦开机就得连续运行若干小时一旦停机也得停够时间。它的标准写法是基于启停状态变化的窗口约束——若 u(t) 从 0 变 1则接下来 T_on 个时段内 u 必须保持为 1停机同理。这条约束的存在意味着调度问题的解空间不再是简单的时间可分离结构前后时段强耦合也是为什么这个问题必须用整数规划而不是逐时段的贪心。2.3 时间尺度日前、日内滚动与实时三层的耦合真实的调度不是一个优化问题是一串。日前层看 24 小时步长 1 小时决定机组启停计划和储能的终端 SOC 目标日内层以 15 分钟为步长做滚动优化预测时域 4 小时、执行时域 1 小时根据最新预测修正功率计划实时层在秒级做功率平衡处理预测之外的波动。三层的耦合点非常具体日前层输出的启停计划日内层不能在滚动中被随意推翻否则机组的机械寿命会被反复启停吃掉日前层设定的日末 SOC是防止模型把电池「掏空换钱」的唯一保险日内层每次滚动只执行第一个时段的指令剩下的丢掉重算这叫滚动时域也是模型预测控制在电力调度里的直接翻版。耦合写错的典型症状是日前层算出来的成本很漂亮日内层一执行就开始频繁调整储能在两个小时内来回充放十几次。原因往往是日前层没有给日内层留调节裕度或者终端 SOC 设得太死。3. 求解路径为什么同一个模型换算法结果差一截模型写完下一步是选求解方式。这里的分歧比想象中大同一个物理问题用混合整数线性规划、用遗传算法、用粒子群跑出来的日运行成本能差 10% 以上而且没有人能一眼看出谁对谁错。理解差异的来源比追求「更先进的算法」重要。3.1 从非线性到混合整数线性分段线性化到底损失了什么火电机组的燃料成本是出力的二次函数储能退化成本也常被写成二次或分段形式。二次项一出现问题就从 MILP 变成 MIQP 或 MINLP求解难度和求解器依赖都上台阶。工程上的标准动作是分段线性化把二次曲线在出力区间上切成 K 段用 K 个非负权重变量 λ 表示落在哪一段上权重之和为 1成本等于各段端点成本按 λ 加权。% 二次成本 f(P) a*P^2 b*P c 的分段线性化K 段割线 K 5; P_brk linspace(P_min, P_max, K1); % 断点 C_brk a*P_brk.^2 b*P_brk c; % 断点处的真实成本 % 对每个时段 t、每台机组 i引入 K1 个权重 lambda(i,t,k) 0 % 约束sum_k lambda(i,t,k) 1 权重归一 % P(i,t) sum_k lambda(i,t,k) * P_brk(k) 出力在断点间插值 % cost(i,t) sum_k lambda(i,t,k) * C_brk(k) 成本在断点间插值代价是什么割线永远在凸函数上方所以线性化后的成本被系统性地高估高估幅度取决于段数 K 和断点的位置。K3 时误差可能到 5%K10 时压到 1% 以内但 0-1 变量和权重变量数量线性增长求解时间呈指数上升。更隐蔽的坑在断点选择上如果断点没落在最小技术出力附近模型会倾向于把出力推到断点之外此时插值失真最严重。这一步真正被牺牲掉的是「精确性保证」。原模型的解有明确的最优性含义线性化之后的最优解只是近似问题的最优解没人能保证它离真最优有多远。可行做法是跑两遍K5 和 K10 各一次如果目标值差异小于 0.5%就认定线性化可以接受。3.2 启发式算法的真实位置它解的其实是另一个问题遗传算法、粒子群、灰狼优化这套东西在小微电网调度里出现频率极高几乎每份代码里都能看到。它们的实现范式是把 24 个时段乘以所有决策变量展平成一条「染色体」解码成一组调度方案再用一个适应度函数评价约束违反量折算成罚函数加进去。这个范式有它天然的局限。罚系数是玄学同一个问题换一组罚系数结果可以天差地别随机种子不定昨天跑出来的方案今天复现不了最关键的是它不提供任何最优性证书你无法判断「跑了 500 代收敛」到底是真收敛还是早熟停滞。而且罚函数法本质上是允许违反约束换取目标改进的一旦罚系数压不住得到的方案会出现功率不平衡、SOC 越界这类硬伤而种群还在朝着「更低的成本」进化。那它还有没有位置有但位置不在主求解器。一种务实用法是给整数规划提供初始可行解也就是热启动让求解器从一个不错的起点开始剪枝另一种是处理那些确实无法线性化的部件比如机组启停的磨损成本、电池的循环寿命非线性模型。把启发式当作「模型里那块没法写成线性形式的部分的补丁」比把它当作万能求解器清醒得多。还有一类场景是启发式真正占优的模型规模大到整数规划求解不动的时候。几十台设备、96 个时段、加上最小启停时间和互斥约束0-1 变量轻松上万此时启发式在可接受的墙钟时间内给出一个次优但可行的解工程上是划算的。3.3 MATLAB 里的实现骨架数据、模型与求解三段式一份可维护的调度代码结构一定是三段数据段负责把所有输入整理成统一的结构体模型段负责拼装系数矩阵求解段负责调用求解器并解析结果。这三段之间只通过矩阵传递信息不共享全局变量——这一点比代码风格更重要因为调度模型改动极频繁全局变量会让每次修改都变成一次考古。% ---- 数据段所有外部输入进结构体禁止散落变量 ---- T 24; % 时段数 dt 1; % 步长h改步长只需要改这一处 Ndg 3; % 可控机组台数 load_fc data.load(:); % 负荷预测长度 TkWh/时段 pv_fc data.pv(:); % 光伏预测长度 TkWh/时段 % ---- 变量段按 [机组; 充电; 放电; 购电; 售电; 弃光] 顺序拼列 ---- nP Ndg * T; % 机组出力 nESS 2 * T; % 储能充、放 nGrid 2 * T; % 购、售 nCurt T; % 弃光 nVar nP nESS nGrid nCurt; % ---- 目标函数燃料 购电 - 售电 弃光惩罚 ---- f zeros(nVar, 1); idx_buy nP nESS 1 : nP nESS T; idx_sell nP nESS T 1 : nP nESS 2*T; idx_curt nP nESS 2*T 1 : nVar; f(idx_buy) price_buy(:) * dt; % 购电成本正 f(idx_sell) -price_sell(:) * dt; % 售电收益负成本 f(idx_curt) penalty_curt * dt; % 弃光惩罚防止无脑弃 % ---- 求解段显式设定间隙和时限不依赖默认值 ---- options optimoptions(intlinprog, ... RelativeGapTolerance, 1e-4, ... % 默认可能偏松显式收紧 MaxTime, 120, ... % 墙钟上限超时返回可行解 Display, iter); [x, fval, exitflag] intlinprog(f, intcon, A, b, Aeq, beq, lb, ub, options);几个参数值得单独说。RelativeGapTolerance 是相对最优性间隙含义是「当前可行解与下界之差占下界的比例不超过这个值就停」。默认值在 1e-4 量级看起来很小但成本基数大的时候1e-4 就是几百块的差距调度人员会问「为什么昨天和今天差这么多」。显式收紧到 1e-5 甚至 1e-6代价是求解时间变长。MaxTime 是后悔药。整数规划在规模稍大时很容易卡在某个节点上磨几十分钟设一个墙钟上限超时后求解器返回当前最优的可行解exitflag 会告诉你这是超时而非optimal。调度系统必须能处理这种「不最优但可用」的结果而不是一直等下去。exitflag 必须检查而且是分支判断。返回 1 表示找到最优返回 2 表示超时但有可行解返回 -2 表示连可行解都没有——这时候拿着 x 去做功率下发是要出事的。常见做法是exitflag 为 2 时降级使用「上一版计划 储能兜底」为 -2 时直接切到规则调度。4. 储能建模SOC 递推里藏着最容易翻车的地方储能是微电网调度里最灵活也最容易写错的部件。它的灵活性来自双向功率它的危险性也来自双向功率——充和放用的是同一套参数方向一错模型就在凭空造能量或者凭空吞能量。4.1 充放电效率的双向折算与自放电SOC 的递推关系写起来只有一行但每个系数的位置都有讲究。充电时电网侧送进去 1 kWh电池里只存进 η_c kWh放电时电池里拿出 1 kWh电网侧只能得到 η_d kWh。所以递推式里充电项要乘 η_c放电项要除 η_d两者不能共用一个效率。$$\mathrm{SOC}(t)\left(1-\sigma\right)\mathrm{SOC}(t-1)\frac{\eta_c P_{ch}(t)\Delta t-\dfrac{P_{dis}(t)\Delta t}{\eta_d}}{E_{cap}}$$其中 σ 是自放电率E_cap 是额定容量。常见的错误写法是把放电项也乘 η_d物理上等于「放电越放越多」结果模型会非常喜欢放电——因为它发现了一条免费的能量来源。另一个错误是漏掉自放电短期看影响不大但做周调度或者长时间序列仿真时SOC 会系统性地偏高。% ---- 储能参数 ---- E_cap 200; % 额定容量kWh P_max 100; % 最大充放电功率kW eta_c 0.95; % 充电效率 eta_d 0.95; % 放电效率 sigma 0.0002; % 自放电率每时段 soc0 0.20; % 初始 SOC soc_lo 0.10; % 下限防止过放 soc_hi 0.90; % 上限防止过充 % ---- SOC 递推的等式约束逐时段拼行 ---- for t 1:T row zeros(1, nVar); row(idx_soc(t)) 1; % SOC(t) if t 1 row(idx_soc(t-1)) -(1 - sigma); % 上一时段衰减后保留 end row(idx_ch(t)) -eta_c * dt / E_cap; % 充电项乘效率 row(idx_dis(t)) dt / (eta_d * E_cap); % 放电项除效率 Aeq(end1, :) row; beq(end1, 1) (t 1) * (1 - sigma) * soc0; % 首时段挂初值 end注意放电项的符号SOC 的增量等于「充进来的电量减放出去的电量」所以放电那一列在等式左边是正的写错一个符号整条链路都反了。这类错误不会让求解器报错只会让它给出一个 SOC 曲线单调上升的奇怪结果而成本还特别低——因为它一直在「反向放电」。4.2 充放电互斥为什么必须引入 0-1 变量只写 SOC 递推不写互斥模型允许储能同一时刻既充又放。有人会觉得这没关系因为同时充放净功率为零而效率损耗会让成本变高优化器自然会避开。这个推理在正电价下基本成立在下面两种情况下却会失效。第一种是有倒送补贴或者负电价的时段。此时多买电、多卖电都有收益同时充放相当于在两条路径上各赚一次损耗被补贴覆盖模型会心安理得地「空转」电池。第二种是当模型里存在某些下限约束时比如并网点必须维持一定的功率因数或者最小交换功率同时充放可以被用来消耗多余的能量而这在物理上是通过发热实现的——现实中电池不会允许。互斥的标准写法是给充电和放电各配一个 0-1 标志然后用大 M 把功率和标志绑在一起M P_max; % 大 M 取最大充放电功率够用即可取太大会松弛 LP for t 1:T % 充电功率 M * z_ch(t) A(end1, :) -[ 1, -M ]; % 对应 [P_ch(t); z_ch(t)] b(end1, 1) 0; % 放电功率 M * z_dis(t) A(end1, :) -[ 1, -M ]; % 对应 [P_dis(t); z_dis(t)] b(end1, 1) 0; % z_ch(t) z_dis(t) 1 A(end1, :) [1, 1]; b(end1, 1) 1; end大 M 的取值是个技术活。取 P_max 是紧凑的取一个远大于 P_max 的数比如 10000会让 LP 松弛变得极松分支定界的下界质量下降求解时间可能翻好几倍。这是整数规划里的血泪经验M 越小越好只要够用。4.3 终端 SOC 约束与循环寿命的折中如果不加终端约束优化器一定会把电池在最后几个时段掏空因为它没有「明天」的概念。这在数学上是最优的在运营上是灾难——第二天早上电池从 10% 开始早高峰顶不住。最简单的做法是硬约束 SOC(T) SOC(0)。问题是它太死如果最后一天光伏特别好、电价特别低模型被迫在日末把电池充满可能反而不经济。折中方案是软约束允许偏差但加惩罚SOC(T) ≥ SOC_target - slackslack ≥ 0目标函数里加一个大的惩罚系数乘 slack。惩罚系数怎么定通常取峰时电价的 5 到 10 倍让模型「除非极其划算否则不偏离」。循环寿命是另一个维度。电池的寿命损耗跟放电深度和循环次数强相关粗略的建模方式是在目标函数里给充放电量加一个退化成本系数每 kWh 吞吐量折算几分钱。这个系数定得太小模型会把电池当调节器使一天充放四五次定得合适模型自然倾向于浅充浅放。我一般会拿厂家给的循环次数—放电深度曲线做一个粗略折算得到一个保守的系数宁可让它少动几次。5. 常见排查调度结果「数学上最优、工程上不可行」的五种现场模型跑通、求解器返回 optimal只是万里长征第一步。真正耗时间的是拿着结果去核对每一条物理约束然后发现某处对不上。下面这五种情况我见过太多次按出现频率排。5.1 功率平衡残差不为零现象把结果里的各单元功率加总和负荷预测对比差值在 1e-3 到 0.1 的量级不是精确的 0但也没大到一眼看出。原因通常是单位与时间步长混用。负荷预测用的是 kWh每 1 小时的能量而机组出力变量是 kW瞬时功率功率平衡式里一边乘了 dt、另一边没乘dt1 的时候看不出来一旦换成 15 分钟步长就立刻暴露。另一种原因是光伏预测给的也是能量但弃光变量减去时忘了对齐量纲。解决在数据段之后加一个断言函数把每一类输入的单位和量纲写成注释并强制转换到统一体系比如「全部转成 kWh/时段」。然后在结果校验里写一句assert(abs(sum(supply) - sum(demand)) 1e-6)跑一次就能定位到底哪条约束漏了。5.2 储能 SOC 越界现象结果里 SOC 出现 1.03 或者 -0.02 这样的值。原因有三类。最常见的是 E_cap 的单位给错比如容量填的是 Ah 而功率是 kW中间少了电压换算导致 SOC 每时段的变化量被放大十几倍。其次是 SOC 的上下限约束只写了 upper bound忘了 lower bound——很多人用 lb/ub 给变量设界SOC 变量只设了 ub结果模型很开心地把它放到 0 以下。第三类是首时段的初值处理(t 1) * soc0这种写法如果 t 的类型是 double 而不是整数逻辑索引会失效。解决把 SOC 的上下限写成显式的 A 矩阵约束而不是只依赖 lb/ub并且在结果解析后打印 SOC 的最小值和最大值。越界一般不会太大能看见就能改。5.3 求解器返回「最优」但违反爬坡现象exitflag 是 1但相邻时段的机组出力差超过了额定爬坡速率。原因通常是爬坡约束的系数没有乘 dt。日前层 dt1 时约束是 P(t)-P(t-1) ≤ R_up日内层 dt0.25 时应该是 P(t)-P(t-1) ≤ R_up × 0.25。如果两层复用同一个约束矩阵日内层的爬坡约束就被放大了 4 倍等于没有约束。另一种原因是启停瞬间的功率跳变没有被特殊处理模型把「从 0 直接到额定出力」当成一次正常的爬坡变化。解决约束矩阵的生成函数必须接受 dt 作为入参不允许从工作区读全局变量启停时刻的爬坡约束用状态变量分段松绑或者把启动过程单独建成一个爬坡率更宽松的时段。5.4 同一份数据跑两次结果不同现象代码没改输入没变两次运行的成本差了几百块调度曲线形状都不一样。原因分两类。启发式算法的随机种子没固定这是最直接的。整数规划这边很多求解器的并行线程数、割平面策略、启发式搜索强度都跟机器状态有关默认参数下确实可能出现细微差异更隐蔽的是默认的相对间隙如果两次运行分别在间隙刚好的时候停止得到的解就不同。解决启发式统一用rng(42, twister)固定种子整数规划显式设定RelativeGapTolerance和线程数把不确定性收窄到一个可接受的范围。要完全确定性得把间隙设成 0 并关掉并行代价是时间。5.5 计算时间失控现象24 个时段、10 台设备的小算例跑了几分钟还没出结果甚至内存吃满。根本原因是 0-1 变量的规模。每个时段每台机组一个启停变量储能两个互斥变量购售电两个互斥变量10 台设备乘 24 时段就是几百个整数变量加上最小启停时间的窗口约束分支定界的节点数是指数级的。解决有三条路。一是聚合把同型号的机组合并成「机组群」用总出力和总台数建模变量数直接除以机组台数。二是简化最小启停时间用「日启停次数上限」这种聚合约束替代逐时段的窗口约束牺牲一点精度换数量级的速度。三是设MaxTime拿可行解调度系统本来就不需要证明最优只需要比人工排班好。6. 设计取舍预测误差、鲁棒性与算力没法同时满足调度模型最终要面对的是预测误差。光伏预测的日前误差通常在 10% 到 20%负荷预测在 3% 到 5%这两个数字决定了确定性优化的天花板——你算得再精确输入本身就是模糊的。工程上有三条常见路线。确定性优化只用一个点预测算得最快模型最简单但预测一旦偏了计划就开始漂。鲁棒优化把预测装进一个不确定集比如 ±15% 的盒子求解最坏情况下的最优好处是任何落在集合内的实际值都不会让计划失效代价是保守——为了应付可能根本不发生的最坏情况日常运行成本会高出 5% 到 10%。随机规划用场景法生成几百个预测场景、做场景削减到几十个求解期望成本最小精度最好但场景数量直接决定模型规模算力吃不消的时候只能砍场景砍完又失去了统计意义。我的判断是这三者不该二选一而应该分层用。日前层用鲁棒优化定启停计划和终端 SOC因为这两件事一旦定了就不好改值得付出保守的代价日内层用确定性滚动优化因为每隔 15 分钟就有新预测进来反馈本身就是一种鲁棒性实时层用规则兜底响应速度比经济性重要。这个分层结构在 MATLAB 里实现起来并不复杂三套模型共享同一份约束生成函数只是不确定集和时域参数不同。验证方法上有个笨但有效的习惯拿历史数据做闭环回放。把某一天的真实光伏、负荷数据按时间顺序喂给调度器每个时段只让它看当前及以前的预测执行它的第一个指令看 SOC 和成本怎么演化。开环算出来的成本和闭环跑出来的成本往往差 8% 以上差额就是预测误差的代价。这一步不做模型的经济性指标全是纸上谈兵。从那以后我每次交付调度模型都强制走一遍闭环回放哪怕算例只有一天——因为没有闭环验证过的「最优」只是求解器眼里的最优。希望帮到你。本文还有配套的精品资源点击获取