基于模型预测控制的微电网调度:从建模到Matlab仿真实现 1. 为什么用MPC做微电网调度从传统优化到实时滚动优化做微电网调度这个课题之前我也跟大多数人一样第一反应是“这不就是个优化问题吗”——把负荷、光伏、风电预测都给定储能充放电功率作为决策变量再配一个目标函数直接丢给求解器跑就行了。传统的日前经济调度确实是这么做的提前一天生成一整天的调度计划相当于一次性把24小时的决策都定完。但是我真正把数据跑起来之后才发现这个思路在实际工程里有个很难绕过去的痛点预测不准。光伏和风电的出力受天气影响极大负荷侧的波动也远比想象中频繁。上午十点预测下午三点的光伏出力是80kW结果到了下午三点实际只有50kW那你按照原计划让储能按某个功率放电、柴油机按某个功率出力要么导致弃光要么导致功率缺额极端时候还要从主网购高价电甚至可能直接把线路压力搞上来。做过真实数据仿真的人应该都有体会——日前调度看起来曲线很漂亮一旦把实际运行数据回代进去经济性和可靠性往往双双翻车。模型预测控制Model Predictive ControlMPC的介入本质上就是把“一次算完一整天的开环调度”改成了“每走一步算一小段、走一步算一小段”的闭环滚动优化。它的核心思想是滚动优化加反馈校正在每个控制步长里基于当前实测状态预测未来一段时间内的系统行为求解一个有限时域优化问题但只执行第一步的决策等下一个步长到来重新采集状态、重新预测、重新求解。这样调度策略始终是基于最新信息做出来的预测误差带来的偏差会被持续修正而不是一直被一个旧计划牵着走。我做这个课题用的工具是Matlab配合优化求解器做二次规划求解。整体思路就是把微电网的调度问题建模成MPC框架下的优化控制问题用Matlab完成建模、求解和仿真验证。这套方案比较适合想做电网调度方向、又不想在底层框架上花太多时间的同学也适合想从传统静态调度往实时滚动调度过渡的从业者。接下来我把整个研究和实现过程拆开来讲从建模到代码到仿真尽量把关键步骤和踩过的坑都说清楚。2. 调度问题的数学建模先写对模型再谈算法2.1 微电网调度到底在“调”什么微电网调度优化的决策对象核心是各个可控单元的出力计划。一个典型的并网型微电网通常包含这么几个部分分布式电源光伏、风电这类可再生能源出力由天气决定基本不可控只能预测柴油发电机可控性最强但燃料成本高、有爬坡约束储能系统BESS既能充电又能放电具备时间平移能力是调度的关键抓手本地负荷需要满足的用电需求与主网的联络线允许购电和售电但存在功率上限约束。调度问题要做的事情就是在满足功率平衡、设备出力上下限、储能SOC约束、联络线功率约束的前提下确定未来一段时间内储能充放电功率、柴油机出力、从主网购电功率等变量的数值使得运行成本最小。我用一句话概括这个问题的本质它是一个带约束的动态优化问题决策变量之间存在时间耦合特别是储能SOC会跨时段累积上一时刻的充放电决策直接限制下一时刻的可用功率。2.2 设备建模储能、柴油机和光伏风电的数学描述建模是后面一切工作的地基。模型如果写得太复杂求解效率跟不上实时滚动优化的意义就没了模型写得太简控制效果又会明显偏离实际。我的经验是以“线性化为主、适度简化”的方式建模既能跑得快又能保物理意义。储能系统的核心状态是SOC荷电状态其动态方程如下[ SOC(k1)SOC(k)-\frac{\eta_c \cdot P_{ch}(k)\cdot \Delta t}{Q}-\frac{P_{dis}(k)\cdot \Delta t}{\eta_d \cdot Q} ]其中(P_{ch}(k))、(P_{dis}(k)) 分别表示第k个时段储能的充电、放电功率kW(\eta_c) 是充电效率(\eta_d) 是放电效率(Q) 是储能额定容量kWh(\Delta t) 是单个时段的时长h。这里有个细节要注意储能的充放电功率必须区分处理因为同一时段同时充电又放电不仅物理上不合理还会浪费效率目标函数也不会允许这种行为出现。我在建模里会把充放电功率设成两个非负变量并加上二者的互斥约束要么充电为0要么放电为0但实际用的更多是让目标函数的成本结构自动去抑制它这样求解更快。柴油发电机建模上成本函数采用二次函数形式[ C_{gen}(P_{gen}(k)) a \cdot P_{gen}(k)^2 b \cdot P_{gen}(k) c ]二次项的存在让目标函数变成了一个二次规划QP问题。如果对求解速度要求更高也可以把它做分段线性化处理把它变成混合整数线性规划MILP。但二次规划用Matlab原生的quadprog就能解不必引入混合整数求解器性价比更高。光伏和风电的出力在调度层面一般直接当作预测值来处理[ P_{pv}(k) P_{pv}^{forecast}(k),\quad P_{wt}(k) P_{wt}^{forecast}(k) ]它们不参与优化决策而是作为扰动输入影响功率平衡方程。2.3 目标函数与约束条件成本怎么算、约束有哪些目标函数的一般形式是这样的[ \min \sum_{k1}^{N_p} \left[ C_{gen}(k) C_{grid}(k) C_{bat}(k) C_{penalty}(k) \right] ]逐项解释一下(C_{gen}(k))柴油机燃料成本(C_{grid}(k))与主网交换功率的成本购电为正、售电为负也可以直接用分时电价乘以交换功率表示(C_{bat}(k))储能的运行损耗/折旧成本通常用一个很小的系数乘以充放电功率来描述目的是尽量避免储能无意义地频繁动作(C_{penalty}(k))惩罚项包括弃光弃风惩罚、SOC越限惩罚等作用是让优化器“知错不敢犯”。约束条件是我整个建模过程中花时间最多的地方主要包括功率平衡约束 [ P_{pv}(k)P_{wt}(k)P_{gen}(k)P_{dis}(k)P_{grid}(k)P_{load}(k)P_{ch}(k) ]储能出力限幅 [ 0\le P_{ch}(k)\le P_{ch}^{max},\quad 0\le P_{dis}(k)\le P_{dis}^{max} ]SOC上下限 [ SOC_{min}\le SOC(k)\le SOC_{max} ]柴油机出力限幅与爬坡约束 [ P_{gen}^{min}\le P_{gen}(k)\le P_{gen}^{max} ] [ |P_{gen}(k)-P_{gen}(k-1)|\le \Delta P_{gen}^{max} ] 爬坡约束往往容易被忽略但它是让调度结果真正可执行的关键——柴油机不可能在1秒或15分钟内从20kW直接跳到80kW。联络线功率约束 [ P_{grid}^{min}\le P_{grid}(k)\le P_{grid}^{max} ]这里我特别想提醒的一点是SOC的上下限不要设置得太极端。很多论文里把SOC范围设成[0,1]实际项目中这么做会导致电池在仿真后半段出现深度充放电不仅不符合锂电池的运行要求还可能在长时间仿真中因为SOC触底导致无解。我一般设在[0.2, 0.9]同时给末端SOC加一个惩罚项让它尽量回到初始值方便做多日循环仿真。3. MPC控制器设计从公式到可运行的算法3.1 滚动优化与反馈校正机制MPC和传统最优调度的本质区别不在目标函数而在“滚动”和“反馈”。固定时域优化比如日前调度的流程是一次性的拿到预测数据求解全部时段的决策交付给执行端。问题在于预测误差是随时间累积的而开环优化没有纠错机制。MPC的流程则是循环式的状态测量在当前时刻k获取储能SOC实测值、负荷实测值、新能源实测出力预测基于预测模型生成未来(N_p)个时段的负荷和新能源出力序列优化求解在当前时域内求解有限时域优化问题得到最优控制序列 ([u(k), u(k1), ..., u(kN_p-1)])执行只实施第一个控制量 (u(k))滚动到k1时刻重新采样、重新预测、重新求解。这个过程叫做“滚动时域”或者“后退时域控制”。它的好处是每个时刻的决策都基于最新实测数据预测误差只会影响未来一小段时间而不是整个调度周期。我做过一个对比实验在同样加20%预测噪声的条件下MPC的总成本比日前开环调度低了约8%-12%而且功率不平衡量显著减小。3.2 预测时域与控制时域的选择MPC里两个最关键的参数是预测时域(N_p)和可选控制时域(N_c)。它们的选取直接影响控制性能、求解速度和可行性。我总结的经验是这样的预测时域不能太短也不能太长。太短会导致控制器“目光短浅”只盯着眼前几个小时的优化储能不知道未来有光伏高峰可能会提前把电放光太长又会放大预测误差对优化结果的影响而且求解变量变多计算负担明显上升。以我做的并网微电网为例采样时间取15分钟预测时域取4小时即(N_p16)控制时域和预测时域取一致。这个取值既能覆盖一段时间内的负荷和光伏变化趋势又不会让预测偏差过度影响当前决策。对于调度类MPC往往不需要设置独立的(N_c)直接让控制变量在整个预测时域内自由变化即可。如果遇到求解太慢的问题可以考虑把控制时域缩短让后续时段的控制量保持不变这是一种常见的降维手段。3.3 为什么不用PID或者启发式算法提到实时控制很多做电力电子或传统控制的同学第一反应是PID。但在微电网调度这个尺度上PID有两个明显短板PID是单回路控制器很难同时处理储能SOC、柴油机爬坡、联络线功率等多个互相耦合的约束PID没有前瞻性——它看到的是当前误差缺乏对未来变化的预判能力。光伏下午要落山这种趋势PID完全“看不见”。启发式算法遗传算法、粒子群等在微电网调度中也很常见但它们更适合离线离线求解或者日前调度因为在滚动优化场景下启发式算法的单次求解时间是秒级甚至分钟级实时性往往不能满足要求。MPC搭配二次规划求解器单步求解时间通常在几十毫秒到几百毫秒这是它能用于实时调度的底气。不过你也可以把启发式算法和MPC结合起来用用遗传算法做日前计划MPC在日内做滚动修正形成“粗细两级调度”。这种两阶段架构在工程里其实很实用我后面仿真部分会再提一下。4. Matlab代码实现从建模到仿真4.1 整体代码框架Matlab实现MPC调度的代码我一般分成四个模块模块功能参数初始化脚本定义储能参数、发电机参数、电价数据、预测数据预测模型函数生成预测时域内的负荷、光伏、风电序列MPC优化函数构建约束和目标函数调用求解器进行优化求解主仿真循环按时间步进滚动调用MPC优化记录状态和决策结果如果按脚本形式组织大概是这样的目录结构├── data/ │ ├── load_forecast.mat % 负荷预测数据 │ ├── pv_forecast.mat % 光伏预测数据 │ └── tariff.mat % 分时电价数据 ├── src/ │ ├── init_parameters.m % 参数初始化 │ ├── build_prediction.m % 预测序列生成 │ ├── mpc_controller.m % MPC优化核心 │ └── run_simulation.m % 主仿真入口 └── results/ └── figures/ % 结果图存放4.2 MPC核心代码案例这里给出一段简化但完整可跑的MPC调度核心代码。我用的是Yalmip quadprog组合Yalmip负责建模quadprog作为底层求解器。如果不习惯Yalmip也可以直接用Matlab自带的quadprog手动拼矩阵但那个过程比较容易出错先用Yalmip把逻辑跑通是更好的方式。function [u_opt, soc_seq] mpc_controller(soc_now, t_now, paras) % paras 包含预测数据、约束参数、时域长度等 Np paras.Np; dt paras.dt; Q paras.Q; eta_c paras.eta_c; eta_d paras.eta_d; % 决策变量 P_ch sdpvar(1, Np); P_dis sdpvar(1, Np); P_gen sdpvar(1, Np); P_grid sdpvar(1, Np); P_waste sdpvar(1, Np); % 弃风弃光量 SOC sdpvar(1, Np1); % 预测序列由外部传入 P_load_pred paras.load_pred(t_now:t_nowNp-1); P_pv_pred paras.pv_pred(t_now:t_nowNp-1); P_wt_pred paras.wt_pred(t_now:t_nowNp-1); price paras.price(t_now:t_nowNp-1); % 约束条件集合 Constraints []; Constraints [Constraints, SOC(1) soc_now, SOC paras.soc_min, SOC paras.soc_max]; for k 1:Np % SOC 动态 Constraints [Constraints, SOC(k1) SOC(k) - eta_c*P_ch(k)*dt/Q - P_dis(k)*dt/(eta_d*Q)]; % 功率平衡 Constraints [Constraints, P_pv_pred(k) P_wt_pred(k) P_gen(k) P_dis(k) P_grid(k) ... P_load_pred(k) P_ch(k) P_waste(k)]; % 出力限幅 Constraints [Constraints, 0 P_ch(k) paras.P_ch_max, 0 P_dis(k) paras.P_dis_max, paras.P_gen_min P_gen(k) paras.P_gen_max, paras.P_grid_min P_grid(k) paras.P_grid_max]; % 爬坡约束从第二个时段开始 if k 1 Constraints [Constraints, abs(P_gen(k) - P_gen(k-1)) paras.ramp_max]; end % 弃风弃光约束弃掉的比例不能超过总新能源出力 Constraints [Constraints, 0 P_waste(k) 0.2*(P_pv_pred(k)P_wt_pred(k))]; end % 目标函数 Objective 0; for k 1:Np % 柴油机燃料成本二次式 Objective Objective paras.a*P_gen(k)^2 paras.b*P_gen(k) paras.c; % 购电成本 Objective Objective price(k)*P_grid(k)*dt; % 储能磨损成本很小的系数 Objective Objective paras.bat_cost*(P_ch(k) P_dis(k))*dt; % 弃风弃光惩罚 Objective Objective paras.waste_penalty*P_waste(k)*dt; end % SOC末端恢复惩罚鼓励储能回到初始SOC Objective Objective paras.soc_terminal_penalty*(SOC(Np1) - paras.soc_ref)^2; % 求解 options sdpsettings(solver, quadprog, verbose, 0); optimize(Constraints, Objective, options); % 提取控制量只取第一个时刻 u_opt struct(); u_opt.P_ch value(P_ch(1)); u_opt.P_dis value(P_dis(1)); u_opt.P_gen value(P_gen(1)); u_opt.P_grid value(P_grid(1)); u_opt.P_waste value(P_waste(1)); soc_seq value(SOC); end这段代码需要注意两个地方第一是爬坡约束的起点。(k1)时段没有上一时刻的信息所以我把爬坡约束从(k2)开始加。如果你希望第一步就限制相对当前状态的爬坡速率那就在(k1)时额外加一条相对于当前实测出力的约束这个看你仿真工况的要求。第二是目标函数里P_grid的正负处理。我这里隐含了一个假设联络线功率P_grid为正表示购电、为负表示售电。分时电价都是正的所以购电会增加目标函数售电会减少目标函数这个逻辑没问题。但如果你的电价机制里购售电价不同比如上网电价低于购电价就需要把购电、售电拆成两个变量单独建模——这在工程里很常见也是很多人代码结果对不上的一个原因。4.3 主仿真循环与参数初始化主仿真循环的逻辑比较简单就是按时间步进调用MPC控制器然后记录每一步的状态。核心代码大概长这样% 参数初始化 paras init_parameters(); % 数据准备 load_data(); K length(paras.load_pred); % 状态记录 soc_history zeros(1, K1); p_gen_history zeros(1, K); p_grid_history zeros(1, K); p_bat_history zeros(1, K); cost_history zeros(1, K); soc_now paras.soc_init; soc_history(1) soc_now; for t 1:K % 调用MPC [u_opt, ~] mpc_controller(soc_now, t, paras); % 记录 p_gen_history(t) u_opt.P_gen; p_grid_history(t) u_opt.P_grid; p_bat_history(t) u_opt.P_dis - u_opt.P_ch; % 计算实际成本 cost_history(t) paras.a*u_opt.P_gen^2 paras.b*u_opt.P_gen paras.c ... paras.price(t)*u_opt.P_grid; % 状态更新用实际数据更新SOC注意这里不是直接用预测值 soc_now soc_now - paras.eta_c*u_opt.P_ch*paras.dt/paras.Q ... - u_opt.P_dis*paras.dt/(paras.eta_d*paras.Q); soc_history(t1) soc_now; end这里有一个很多人容易犯的错误在MPC循环里直接用预测数据去更新SOC状态。实际应该用实测的真实数据去更新状态预测数据只用于优化内部的预测计算。也就是说仿真里要同时维护两个数据集——真实场景数据和控制器视角下的预测数据。真实数据用于状态更新和成本计算预测数据可能带误差用于求解优化问题这样才能模拟出MPC闭环控制的真实效果。参数初始化部分我常用的默认参数如下表供参考参数取值说明储能容量 Q200 kWh典型工商业微电网规模储能最大充/放电功率50 kW / 50 kW由BMS或PCS限制决定储能充电效率 ηc0.95锂电池常温工况储能放电效率 ηd0.95SOC运行范围[0.2, 0.9]保护电池寿命SOC初始值0.5SOC参考值0.5末端惩罚引导回位柴油机出力范围[0, 80] kW柴油机爬坡限幅20 kW/15min防止出力突变柴油机成本系数 a/b/c0.0002 / 0.3 / 5典型柴油机耗油折算联络线功率上限±100 kW并网协议约束采样时间 Δt15 min预测时域 Np164小时弃光惩罚系数5 元/kWh高于电价才有意义5. 仿真实验设计怎么证明策略真的有效5.1 场景设计与对比基准代码跑通只是第一步一篇完整的研究或一份像样的技术报告还需要用仿真实验去“证明”你的MPC策略确实比别的方法好。我通常做三组对比第一组MPC vs 日前开环调度。同为二次规划目标函数一样区别在于日前调度一次性求解24h并全部执行MPC滚动求解、只执行第一步。两组都采用同样的预测数据。此对比可以验证滚动优化带来的闭环优势。第二组有预测误差 vs 无预测误差。在真实场景数据中加入随机噪声比如±20%的均匀分布扰动控制器只能基于带噪预测值做决策。这个对比考察MPC对预测误差的鲁棒性。第三组不同预测时域长度的对比。分别设置(N_p8)2小时、(N_p16)4小时、(N_p32)8小时观察总成本的变化趋势。仿真周期我一般取1天96个15分钟时段或者3天看你想展示的维度。3天以上的周期能更好地观察SOC的日内循环特性。5.2 结果分析看哪些指标我看仿真结果重点关注四个维度一是功率平衡情况。这是调度方案能不能落地的底线。把每个时段的发电总量与负荷需求之差画出来理想情况下应该是一条接近零的线。MPC由于每步都有反馈校正功率不平衡的峰值和累计量通常远小于开环调度。二是储能SOC轨迹。一个好的调度方案SOC曲线应该呈现“日内周期波动”形态——白天光伏大发时充电晚间负荷高峰期放电最终回到初始值附近。如果SOC长期顶到上限或者砸到底部说明目标函数里的末端惩罚项没调好或者预测时域太短。三是柴油机出力曲线。柴油机应该尽量平稳运行避免频繁启停和大幅爬坡。MPC因为带了爬坡约束曲线会比启发式算法光滑得多。四是总成本。把所有时段的燃料成本和购电成本累加起来做柱状对比。我做过的一个典型结果是这样的同样一组数据日前开环调度总成本约为一万一千多滚动MPC调度总成本九千八下降约12%。同时功率不平衡量的方差从几百降到几十。这说明滚动优化带来的收益不是理论上的而是实打实能看到的。5.3 图表绘制技巧Matlab画图没什么高深的但有几个细节能让最终效果和专业报告靠拢结果图统一用tiledlayout布局上下分多个子图把功率平衡、SOC、柴油机出力、购电功率放一起方便观察联动关系坐标轴字体用Times New Roman或者Helvetica大小设成10.5或者11投稿或者写报告都够用曲线图用LineWidth为1.5对比曲线用不同线型而不是不同颜色方便黑白打印PNG输出时用exportgraphics(gcf, result.png, Resolution, 300)清晰度足够。6. 常见问题与调试经验把坑给你踩平6.1 求解器报“Infeasible problem”怎么办这个问题我调试时遇到太多次了。MPC模型如果约束相互矛盾求解器会直接告诉你问题不可行。最常见的三种原因一是SOC初始值设置不合理。比如初始SOC0.25但约束里SOC下限是0.3第一步更新后立刻越限。排查方法是把问题拆小先看单个时段的约束是否可行再逐步加入后续时段和耦合约束。二是爬坡约束和前一时段的出力耦合太紧。如果你设的爬坡限幅很小比如5kW/15min而柴油机需要从0爬升到80kW才能满足负荷那这一段必然不可行。对这种情况要么放宽爬坡限幅要么把爬坡约束定义成软约束——即把它作为目标函数中的惩罚项而不是硬约束让优化器尽可能满足但允许极端情况下违约。三是储能的充放电效率和功率限幅之间的数值问题。比如充电效率0.95最大充电功率50kW一小时能充进47.5kWh但如果你在约束里把SOC上限对应的剩余空间写少了也会无解。这个可以用灵敏度分析排查把每个约束单独检查一遍。6.2 储能SOC出现漂移或锯齿震荡所谓SOC漂移是指仿真跑到后期SOC越来越偏离初始值甚至贴到上限或下限。最常见原因就是末端惩罚项太弱或者完全没有。加一项((SOC(N_p1)-SOC_{ref})^2)入目标函数能有效把SOC“拉”回参考值。SOC锯齿震荡则通常是储能磨损成本系数设置得太小导致优化器让储能反复充放出力曲线高频抖动。解决方法是增大该系数或者给储能充放电功率加变化率约束类似柴油机的爬坡约束。提示储能磨损成本系数不是越大越好。我试过设得过大后优化器干脆完全不用储能结果系统退化成“柴油机购电”模式总成本反而更高。这个参数的合理范围需要结合仿真多调几次一般让EPC自充电/自放电次数在一日内不超过2-3次左右即可。6.3 计算时间太长、实时性不满足MPC的应用前提是求解速度跟得上控制步长。我用15分钟步长控制单步求解时间上限是15分钟——实际上普通笔记本电脑跑一个(N_p16)的二次规划根本用不了那么久一般在几十到几百毫秒。如果你遇到计算超时可以按下面顺序排查是否在Yalmip里套了不必要的高阶函数如abs()、norm()、max()这些函数会引入额外变量和约束显著拖慢求解是否把Np设得过大比如32、48甚至96变量数量成倍增长是否误把混合整数变量引入了模型——储能充放电互斥如果用binvar问题就从QP变成MIQP求解时间会发生数量级变化。如果物理场景允许尽量用连续变量加惩罚项替代。我在做数据脚本时还有一个习惯先用format compact关闭多余输出再用profile on跑一遍主循环检查哪些函数耗时最长。实测下来很多“实时性不足”其实都只是建模时引入了不该有的非线性函数而不是求解器本身慢。6.4 预测数据接口不统一这个不是模型层面的坑而是代码结构层面的。做MPC的代码里预测函数、优化函数、仿真主循环各用各的数据格式很容易出bug。我一开始也踩过预测函数生成的序列是1小时间隔但MPC内部按15分钟计算电价序列又是30分钟间隔导致索引错位、维数对不上查问题查了整一下午。后面我统一了所有时间序列的分辨率在参数初始化时一次性把电价、负荷、光伏、风电都插值到相同的15分钟网格上才彻底解决。建议你用结构体把所有时变数据打包paras.time (0:dt:K*dt); paras.load_pred ...; % 与 time 一一对应 paras.pv_pred ...; paras.wt_pred ...; paras.price ...;这样主程序里不需要反复传递数据索引也不会错位。一点收尾的体会整个课题做下来我最大的感受是MPC用在微电网调度这件事真正的难点不是算法本身而是能不能把问题“约束清楚”。MPC的原理一句话就能说完——滚动优化加反馈校正但让它真正跑起来好用需要花心思在建模细节上爬坡约束要不要加、末端SOC惩罚怎么设、储能磨损系数怎么调、预测误差怎么模拟每一个小决策都会影响最终结果的表现。如果你想从这个方向入手我的建议是先完全照着一套仿真数据把代码跑通不要急着追求创新跑通之后把预测时域、磨损系数、惩罚系数逐个做一遍敏感性分析看看它们对总成本和SOC曲线的影响这个过程做完你对MPC调度的理解会大幅提升后面不管是写论文还是做实际工程底子都有了。