如果你也做微电网的调度优化研究,MPC(模型预测控制)这个名字八成绕不开。我最近把一套完整的MPC微电网调度优化方案用Matlab跑了一遍,从建模、代码实现到调参踩坑完整走下来,发现网上讲原理的多,讲实际落地细节的少。本文会结合我自己写的代码和实验数据,把MPC怎么做调度、目标函数和约束怎么列、滚动时域怎么实现、参数怎么调、哪些坑绝对不能踩,一次性说清楚。适合正在做微电网方向研究的学生,也适合想从规则控制转向优化控制的一线工程师参考。
1. 为什么微电网调度要用MPC:规则控制、单点优化都不够
1.1 先看传统做法差在哪
微电网调度说白了就是要决定“每一时刻储能充多少、从电网买多少电、售多少电”。很多现场项目还在用规则控制:电价低就充,电价高就放,光伏出力大于负荷就优先自用。这套逻辑简单,但有个致命问题——它只看当前时刻,不看未来。午间光伏大发,储能如果上午已经充满,到了午后只能眼睁睁看着光伏白白弃掉;晚高峰电价最高时,储能又因为之前放得太猛而没电可放。规则控制只能保证“当前合理”,无法保证“全天最优”。
单点优化则是另一条路:把一天24小时作为一个整体,一次性求出所有决策变量的最优值。这种方法在“计划完美”的前提下确实能找到全局最优解,可一旦光伏预测偏差大、负荷突增,或者电价临时波动,计划就失效了,必须重新求解,而重新求解后的结果往往会和上次的调度计划冲突。
这两种方法的本质问题是一样的——缺少对“未来不确定性”的闭环处理机制。MPC正好弥补这一点。
1.2 MPC的“预测+滚动+反馈”是怎么工作的
MPC的核心思想可以概括为三句话:预测未来、优化当前、反馈校正。用导航软件来类比最直观:你从A开车去B,导航每5分钟根据实时路况重新规划一次路线,而不是出发前把一整天的路线定死。MPC也是这样——在当前时刻,基于预测模型推算未来一段时间的系统状态,求解一个有限时域的优化问题,但只执行第一步控制动作;下一时刻,用实际测量到的系统状态更新模型,重新优化,再执行第一步。这个过程叫滚动优化(Receding Horizon)。
在微电网里,这个“系统状态”通常就是储能电池的SOC,控制动作就是储能充放电功率、购售电功率,预测模型就是SOC的状态转移方程,未来信息就是光伏出力、负荷需求、分时电价。每一步都基于最新数据重新算,天然具有抗预测误差的能力。
1.3 MPC在微电网里的三个硬优势
我实际做过对比后,对MPC的优势有了很直观的认识。
第一是约束处理能力。储能SOC必须在20%到95%之间,充放电功率不能超过额定值,变压器容量有限制,烟煤机组出力有爬坡限制——这些在传统比例控制器里很难系统化处理,但在MPC里就是一个个线性约束不等式,求解器直接帮你算进去。第二是前瞻性。正是因为能“看到未来”,MPC会主动在午间光伏大发时预留储能容量,在电价低谷提前储电,而不是等事情发生了再纠正。第三是鲁棒性。单点优化一步到位,MPC走一步看一步,预测偏差会导致它每一步都在修正,所以即便光伏预测误差达到20%,实际运行曲线也不会差得离谱。
2. 把调度问题变成数学:状态、目标、约束怎么列
2.1 微电网调度场景的基本架构
建模之前先明确物理结构。我用的算例是一个典型的光储微电网:光伏发电单元(PV)、储能系统(ESS)、本地负荷,通过公共连接点(PCC)与配电网交互,另外配了一台柴油发电机作为备用电源。这个结构在Matlab代码里就对应几组变量:光伏出力序列P_pv、负荷序列P_load是外部给定的扰动输入;储能充放电功率、柴油机出力、购售电功率是待优化的控制变量。
调度周期设为24小时,采样时间Ts=1小时。为什么要选1小时?因为光伏和负荷的预测数据通常以小时为粒度,电价时段划分也是小时级,太细了数据跟不上,太粗了储能状态转移失真严重。如果你做日内滚动,可以把Ts设为15分钟,结构完全一样。
2.2 列目标函数:花钱最少,还要平抑波动
目标函数是整个优化问题的“指挥棒”。我用的是运行成本最小化加上惩罚项,具体分为四部分贡献。
第一部分是购电成本,也就是从电网买电花的钱:sum(price_buy(k) * P_buy(k) * Ts),price_buy是分时购电价,P_buy是购电功率,单位一致化后求和。第二部分是售电收益,微电网光伏出力超出负荷时可以向电网售电,收益为sum(price_sell(k) * P_sell(k) * Ts)。注意售电价格通常低于购电价格,中间有价差。第三部分是电池健康度管理,用SOC偏离目标值的二次惩罚来表示:sum(w_soc * (SOC(k) - SOC_ref)^2)。电池过度充放会显著缩短寿命,这个惩罚是必要的。第四部分是功率波动惩罚,用储能功率变化的二次项sum(w_fluct * (P_es(k) - P_es(k-1))^2)来限制充放电功率突变,避免控制动作“抖”得太厉害,对设备寿命和电网冲击都不好。
把这几项加权组合,就得到了MPC每一步要求解的二次规划(QP)目标函数。为什么是二次规划而不是线性规划?因为SOC惩罚项和波动惩罚项本身是平方项,而且二次目标函数能保证控制量平滑,工程上更好用。
2.3 约束条件:电量平衡、SOC、功率上下限、交互钳制
约束条件决定了优化结果是否物理可行。我常用的约束如下表所示:
| 约束类型 | 表达式形式 | 物理含义 |
|---|---|---|
| 功率平衡 | P_buy - P_sell + P_pv + P_dch - P_ch = P_load | 任意时刻微网内功率守恒 |
| SOC状态转移 | SOC(k+1) = SOC(k) + (η_ch * P_ch(k) - P_dch(k) / η_dis) * Ts / E_cap | 储能动态特性 |
| SOC上下限 | SOC_min ≤ SOC(k) ≤ SOC_max | 保护电池,防过充过放 |
| 充放电功率限幅 | 0 ≤ P_ch ≤ P_es_max,0 ≤ P_dch ≤ P_es_max | 储能功率不超过额定值 |
| 电网交互限幅 | 0 ≤ P_buy ≤ P_grid_max,0 ≤ P_sell ≤ P_grid_max | 变压器容量限制 |
| 充放电互斥 | P_ch * P_dch ≈ 0(可通过目标函数隐式约束) | 不允许同时充放电 |
这里有个细节值得提一下:充放电互斥约束在严格建模时需要引入0-1整数变量变成混合整数规划,但实际工程中,只要购电价和售电价之间存在正差价,优化结果天然不会出现同时买卖的情况,所以大多数情况下用线性约束就够了。如果你的场景出现价格倒挂(售电价高于购电价),那就必须加二值变量了。
2.4 离散状态空间形式与Matlab表示
MPC标准形式要求把系统写成状态空间表达式。在这里,状态变量选SOC,控制变量选P_buy、P_sell、P_ch、P_dch、P_diesel,扰动输入选P_pv和P_load。离散状态方程可以写成:
SOC(k+1) = SOC(k) + [η_ch*P_ch(k) - P_dch(k)/η_dis] * Ts / E_cap
写成矩阵形式就是x(k+1) = Ax(k) + B_uu(k) + B_d*d(k)。A是单位矩阵,B_u是充放电系数对应的控制矩阵,B_d对应光伏和负荷扰动的影响。注意P_pv和P_load不是控制变量,而是外部输入,必须放到B_d里,这样求解器才能把它当作已知序列带入预测模型。
在实际Matlab代码里,我的做法是直接用循环结构表达状态迁移,不用手推矩阵系数,这样更直观,改参数也好改。矩阵表达更多用于推导和论文书写。
3. Matlab代码实现:从单步优化到滚动时域
3.1 开发环境与求解器选型
实现MPC的Matlab方案有两种主流选择。一种是直接用Model Predictive Control Toolbox里的mpc对象,适合标准场景,配置起来快,但定制程度有限,加自定义惩罚项很麻烦。另一种是用YALMIP或CVX作为建模层,搭配通用二次规划求解器,灵活度最高。我推荐后者,因为微电网调度问题的约束和成本项往往高度定制化,用通用建模框架反而省力。
求解器我用的是OSQP,一个开源的一阶QP求解器,处理几百个变量的小规模问题速度极快,单步求解在毫秒级。如果你的Matlab版本没有自带OSQP,用内置的quadprog也行,就是大规模时略慢一点。安装YALMIP很简单,把下载的文件夹加入Matlab路径即可。
3.2 预测模型怎么写
下面是我整理过的核心代码结构,可以直接套用:
%% 参数定义 Ts = 1; % 采样时间,小时 Np = 24; % 预测时域长度 E_cap = 500; % 储能容量,kWh eta_ch = 0.95; % 充电效率 eta_dis = 0.90; % 放电效率 SOC_min = 0.2; SOC_max = 0.95; P_es_max = 100; % 储能额定功率,kW P_grid_max = 500; % 变压器容量上限,kW %% 每个采样时刻调用一次优化 for k = 1:24 % 读取当前时刻实测SOC、未来光伏预测、负荷预测、分时电价 SOC_now = SOC_measured(k); P_pv_future = P_pv_forecast(k : k+Np-1); P_load_future = P_load_forecast(k : k+Np-1); price_buy_future = price_buy(k : k+Np-1); % 定义决策变量 P_buy = sdpvar(Np, 1); P_sell = sdpvar(Np, 1); P_ch = sdpvar(Np, 1); P_dch = sdpvar(Np, 1); SOC_var = sdpvar(Np+1, 1); % 目标函数与约束 Objective = 0; Constraints = []; Constraints = [Constraints, SOC_var(1) == SOC_now]; for t = 1:Np % SOC状态转移 Constraints = [Constraints, ... SOC_var(t+1) == SOC_var(t) + ... (eta_ch * P_ch(t) - P_dch(t)/eta_dis) * Ts / E_cap]; % SOC上下限 Constraints = [Constraints, ... SOC_min <= SOC_var(t+1) <= SOC_max]; % 功率平衡 Constraints = [Constraints, ... P_buy(t) - P_sell(t) + P_pv_future(t) + P_dch(t) - P_ch(t) == P_load_future(t)]; % 储能功率限幅 Constraints = [Constraints, ... 0 <= P_ch(t) <= P_es_max, 0 <= P_dch(t) <= P_es_max]; Constraints = [Constraints, ... 0 <= P_buy(t) <= P_grid_max, 0 <= P_sell(t) <= P_grid_max]; % 目标函数累加 Objective = Objective + ... price_buy_future(t) * P_buy(t) * Ts - ... 0.85 * price_buy_future(t) * P_sell(t) * Ts + ... 0.1 * (SOC_var(t+1) - 0.5)^2; end % 求解并只执行第一步 ops = sdpsettings('solver', 'osqp', 'verbose', 0); optimize(Constraints, Objective, ops); P_es_out(k) = value(P_ch(1)) - value(P_dch(1)); P_grid_out(k) = value(P_buy(1)) - value(P_sell(1)); % 更新下一时刻真实SOC(这里默认模型无误差,实际应加入测量值) SOC_measured(k+1) = value(SOC_var(2)); end这段代码的关键点在于:每个时刻都定义一个新的Np步优化问题,求解后只取P_ch(1)和P_dch(1)作为实际执行量,后面Np-1步全部丢弃。这看起来“浪费”,但正是滚动优化的精髓——每走一步就用最新实测状态修正模型,不会把预测误差一路累积到底。
3.3 滚动优化的主循环逻辑
主循环里有一个容易被忽略的细节:SOC_var(1) == SOC_now这个约束,用的是当前时刻的实测值,而不是上一轮优化计算出的SOC_var(2)。这一步是反馈校正的关键,一定要写对。如果直接把模型计算值代入,就退化成开环优化了,失去了MPC闭环控制的优势。
另外我建议把P_pv_forecast向量在每一步都重新赋值,而不是一次性把24小时的预测值全部读进来。这样之后的扩展才能自然支持“滚动预测”,也就是未来预测序列随时间更新的场景。
3.4 一组日调度结果对比:MPC vs 规则控制
我在同一组数据下分别跑了一个“低电价充电、高电价放电”的规则控制器和一个Np=24的MPC控制器,全天运行结果对比如下:
| 指标 | 规则控制 | MPC控制 |
|---|---|---|
| 日购电成本(包含售电收益) | 3186元 | 2698元 |
| 储能日均等效循环次数 | 1.21次 | 0.94次 |
| 最大功率波动幅值 | 62 kW | 31 kW |
| 弃光率 | 7.3% | 1.8% |
MPC在成本上省了约15%,储能循环寿命压力更小,功率波动也明显平缓。成本节省主要来自两个场景:第一,规则控制在下午4点就把SOC放到了最低,但晚高峰电价更高,MPC会算着未来电价趋势留一部分电量到19点到21点再放;第二,午间光伏大发时,规则控制因为光伏不足以为整个储能充满而不敢充太多,MPC却知道下午还有一段时间光伏出力会维持高位,果断提前充电,弃光量大幅下降。
上述场景是我为了对比效果设置的典型算例,预测数据无误差时MPC确实能达到这种效果。如果引入实际预测误差,MPC的滚动机制仍能维持大部分优势。
3.5 扩展思路:日前+日内两级MPC
如果你继续深入研究,你会发现单层MPC在24小时预测时域下计算负担偏大,而且全天预测数据在上午预测夜间的光伏精度很差。我实际试下来比较好的做法是两级调度:日前层用MPC求一个24小时的经济最优计划,时间颗粒度1小时,主要决定“哪天充多少、什么时候放”的宏观策略;日内层用MPC做15分钟级别的滚动修正,预测时域只取4到8步,根据最新光伏实测数据调整储能功率,跟踪日前计划的偏差尽量小。
两层之间的协调在代码里就是两个循环嵌套的问题,外层算完把SOC参考值传给内层作为软约束。这个小改动能使实时响应速度大幅提升,也更能贴近现场控制器节拍。
4. 参数整定与避坑心得
4.1 预测时域Np和控制时域Nc怎么选
Np是MPC调度里最敏感的参数之一。Np太小,MPC变成“近视眼”,只看得到眼前一两步,无法为晚高峰预留电量,退化成规则控制;Np太大,预测后期数据精度极差,而且求解规模变大,在线计算时间会变得不可接受。
我给出的经验值:对于小时级Ts的日前调度,Np选24到36,覆盖完整的一天;对于15分钟级Ts的日内滚动,Np选4到8,覆盖未来1到2小时即可。控制时域Nc在标准的调度类MPC里一般取和Np相同,因为微网调度每个时刻只有一个决策批,不需要像轨迹跟踪那样刻意缩短控制时域。
如果Np=24导致求解偏慢,可以先检查约束是否写得过于冗余,或者把SOC_idle约束松弛成软约束。多数情况下问题不在求解器性能,而在于规模没有控制好。
4.2 目标权重怎么给:成本系数、SOC惩罚、波动惩罚
权重设置是MPC最容易翻车的地方,比算法本身还容易出问题。总结下来有三条经验值得记住。
第一,经济成本项的权重必须和真实电价对齐,这是主目标,其他惩罚项的权重以“不干扰主目标大方向”为原则。设得太大会出现“该买的电不买、该放的电不放”的怪事。第二,SOC惩罚项里的SOC_ref不能定得太理想,我最初设0.5固定值,结果优化器为了让SOC全程保持在0.5附近而牺牲了大量经济效益。后改成动态参考值,把日前计划算出的SOC序列当作参考基准,效果立刻好很多。如果你只要经济最优,甚至可以把SOC惩罚权重设成很小的值,只留上下限约束。第三,波动惩罚系数w_fluct要量级匹配。我用的是充放电功率二阶差分,系数设在0.1到1之间,具体数值需要做参数扫描实验来确定,大一个数量级就会让储能动作迟缓,电价峰谷都来不及反应。
4.3 关键参数速查表
| 参数 | 典型值 | 调参影响 |
|---|---|---|
| Ts | 1小时/15分钟 | 越小响应越快,计算量越大 |
| Np | 24/8 | 越大前瞻性越强,数据精度要求越高 |
| SOC_min/SOC_max | 0.2/0.95 | 放宽可提升经济性,缩小可延长寿命 |
| SOC_ref | 日前计划SOC | 偏离过大影响经济性 |
| 购/售电价差系数 | 0.85 | 控制套利积极性 |
| w_fluct | 0.1~1 | 同样影响控制量平滑度和经济性平衡 |
5. 踩坑实录:5个典型问题与排查方法
5.1 求解器报infeasible怎么办
infeasible是所有接触MPC的人最先遇到的坑,意思是约束集合互相矛盾,找不到可行解。最常见的诱因有两个:一是SOC状态转移约束和SOC上下限约束冲突,比如SOC在0.2,但储能最小充电时段内模型要求它升到0.95,而该时段功率上限又不够,解就不存在;二是功率平衡约束和储能功率上限冲突,某个时刻负荷很高,同时变压器容量又被限制得很紧,储能又饱和或没电,平衡方程无解。
排查方法也简单:先检查所有约束对应的物理量纲是否统一,kW和kWh混用是最常见的隐蔽问题。其次把SOC_min放宽到0.15、P_grid_max加到800,看看问题是否消失,如果消失就说明约束过紧。最后建议在代码里输出求解器返回的diagnostics结果,把yalmip的“relaxed solution”打开,能看到是哪条约束被破坏。
5.2 控制量一抖一抖,储能功率持续往复
这是个很经典的MPC毛病。滚动优化每走一步都重新计算,得到的是一个全局最优轨迹,但每一段的第一点拼起来可能不是平滑曲线,尤其在电价临界点附近,MPC会在“充电还是放电”之间反复横跳,导致储能功率频繁换向。
有三个办法可以压住这个问题。第一,把波动惩罚系数加大,这是最直接的手段。第二,增加约束限制两次决策之间储能功率的最大变化量,比如|P_es(k)-P_es(k-1)|≤20 kW,相当于给控制量加了“变化率约束”。第三,把充放电动作设置滞回区间,SOC低于0.4才允许从放电切到充电,高于0.6才允许从充电切到放电。
5.3 结果对初值SOC特别敏感
这个问题在单次仿真里不明显,但在连续调度里很突出。如果初值SOC从0.5改成0.8,MPC给出的当日调度策略会完全不一样,前几小时的充放电行为差异巨大。原因在于当前SOC直接决定了后续可行域,而MPC只往前看有限步,无法像全局优化一样从全天角度“纠偏”。
解决办法是强化SOC惩罚项,让优化器在满足经济效益的前提下尽量往SOC_ref靠拢,或者把终端状态约束加上——在预测时域末端强制SOC在某个范围内,避免调度结束能量耗尽。在工程应用中,建议给SOC_ref做一个缓慢过渡的变化序列,不要突然大跳变。
5.4 仿真很漂亮,接入系统却不对
仿真结果好,实际接入系统就表现异常,这个问题在MPC落地时太常见了。多数原因出在预测数据和实测数据的衔接上:仿真时用的是同一组历史序列反复预测,实际运行时预测模型却在持续更新,如果预测更新频率和优化执行频率不一致,控制器就会“精准备优化一个错误未来”。
我在代码里专门加了一段逻辑:用当前时刻实测的光伏出力替换预测序列的第一个点,而不是直接使用预测值。这个小改动让整个闭环仿真稳定了很多。如果你也在做闭环仿真,一定要把“实测反馈”显式建模进去,否则验证结果没有参考价值。
5.5 在线计算时间太长
调度步长1小时,单次优化即使跑几十秒也还勉强能接受,但如果要做日内15分钟滚动,单步超过3秒就会积压。我实测下来,Np=24规模的QP问题在普通笔记本上用OSQP求解不到50毫秒,真正耗时的反而是YALMIP建模部分。YALMIP每次调用都需要重建整个优化对象,这部分时间开销远大于求解本身。
如果建模耗时成了瓶颈,可以考虑把常数矩阵离线提取出来,用matlab自带的quadprog直接求解标准二次规划,省掉YALMIP的建模开销,速度能快两个数量级。前提是你已经调试好目标函数和约束,不再需要频繁修改结构。
我在实际做调度项目时最大的体会是,MPC的代码实现本身并不难,难的是把模型、约束、目标、参数这一整套系统工程问题理顺。核心算法跑通只是第一步,跟预测数据、电池模型、控制器节拍形成闭环才是真正有价值的工程工作。如果后续你想继续扩展,可以从鲁棒MPC、随机MPC、以及与深度学习的结合方向入手,但先把本文这套基础跑稳,比什么都重要。