
微电网的调度优化说白了就是在满足负荷需求和安全约束的前提下决定光伏发的电是直接供给负荷、存进电池还是弃掉储能什么时候充、什么时候放什么时候从大电网买电最划算。这几件事要同时安排传统的做法是拿全天预测数据做一次开环最优调度然后按计划执行。但光伏预测经常会因为天气变化而失准计划一执行就偏离预期。模型预测控制MPC换了个思路走一步算一步每15分钟重新做一次短期优化并用实时测量值校正模型把预测误差造成的影响压到最小。这篇文章记录的就是我从零搭建“基于模型预测控制MPC的微电网调度优化”Matlab实现的全过程包括数学建模、MPC核心逻辑、代码结构和一堆实测踩坑记录适合正在做微电网能量管理、储能协调控制的研究生和工程师借鉴。1. 微电网调度优化的核心需求拆解1.1 要解决的问题为什么不是“把电发出来”那么简单先把这个问题的边界说清楚。并网型微电网通常包括光伏阵列、储能电池、本地负荷以及和大电网之间的公共连接点。调度优化要做的事情本质上是一个带约束的经济性问题在光伏出力、负荷需求已知或可预测的前提下确定每个时间步长内各单元的功率设定值使得整个系统的运行总成本最低同时保证功率平衡、储能SOC不出界、功率不越限。听起来像是一个普通的优化问题但为什么实际做起来麻烦因为光伏出力和负荷用电都是不确定的。云飘过来光伏出力瞬间掉一半负荷侧可能突然启动一台大功率设备。如果调度策略是一次性算完24小时然后开环执行那预测一旦不准后面所有步骤都会跟着错储能可能提前放空也可能出现不必要的弃光。这就是微电网调度比传统大电网计划调度更难的点不确定性不仅是外部扰动还会直接影响控制效果和经济性。1.2 MPC相比传统开环调度的优势在哪传统最优调度的范式是把全天数据交给优化器算出一份完整的功率计划然后照着执行。MPC的范式完全不一样它在每个采样时刻只优化未来有限时域执行当前时刻的第一步然后到下一个时刻重新测量状态、重新优化。这个“滚动时域反馈校正”的结构等于把一次大型优化摊成了很多个小优化每个小优化都基于最新的状态和最新的预测信息天然具备抵抗不确定性的能力。用一个生活化的类比来理解传统调度相当于出发前用导航规划好全程路线然后不看路况直接开MPC则是每到一个路口都用实时路况重新计算接下来几公里的路线虽然每次都只看眼前但整体路线反而更稳、更接近最优。在微电网里“路况”就是光伏预测和负荷预测的偏差MPC的反馈机制能把这些偏差逐步消化掉。1.3 这套Matlab项目最适合谁参考这套代码实现最合适的应用对象有两类。一类是刚接触微电网能量管理的研究生可能已经看过很多MPC文献但不知道代码怎么组织目标函数怎么写、滚动循环怎么实现往往卡在从公式到代码的第一步另一类是做微电网控制系统初期原型验证的工程师需要快速评估MPC算法在典型场景下的调度效果和经济性先用仿真把算法逻辑跑通后面再移植到实时系统。看这篇文章之前不需要太高深的背景但是最好对三件事有概念一是微电网的基本组成知道储能和光伏分别起什么作用二是线性规划和二次规划的基础形式知道约束和目标函数是什么三是Matlab的基本语法最好用过Yalmip或类似的优化建模工具。如果这些基础还不太牢后面每一节也都有对应的解释跟着操作也能跑通。2. 数学模型设计MPC调度的第一步2.1 微电网拓扑和单元建模怎么搭我做的这个仿真项目采用的是一个典型的并网型微电网拓扑分布式光伏单元、储能单元、本地负荷单元统一用一条交流母线连接再通过公共连接点与大电网相连。在不做潮流计算的前提下各单元可以简化为有功功率的平衡关系电压和无功问题先不展开。也就是说建模的重点放在有功功率流向上。光伏单元在MPC框架里一般不作为连续决策变量而是作为预测的外部输入序列。它的特性是出力受光照影响波动性强在调度问题里头可以作为已知参数注入预测模型。储能单元是模型里的核心动态环节状态量是荷电状态SOC控制量是充放电功率。SOC的离散动态方程是MPC内部状态预测的基础数学形式就是状态转移方程下一时刻的SOC等于当前SOC减去充放电导致的能量变化除以电池容量。负荷在调度问题中同样是外部输入不过它比光伏更规律一些早晚有峰、夜间偏低。在仿真中光伏和负荷都可以用预设数据序列表示MPC对它们不直接加以控制只作为预测信号参与各时段的功率平衡计算。2.2 目标函数的每一项都是怎么来的目标函数是整个优化问题的“指挥棒”。在这个项目里我把目标函数设计成四个组成部分叠加。第一项是大电网购电成本。功率从公共连接点流入时按当天时段电价计费目标函数希望这项成本尽量小。这里用分段电价比如峰时段1.0元/kWh、平时段0.6元/kWh、谷时段0.3元/kWh目的是让优化算法自发地在谷时段多存电、峰时段多放电。第二项是SOC参考值偏差惩罚。目的是让储能尽量避免工作在极端电量区间因为长期接近0%或100%都会影响电池寿命。用一个二次函数把SOC拉向参考水平参考值我设成0.5相当于引导算法“保持一定余量”。第三项是储能充放电功率的二次惩罚。注意这里惩罚的是功率本身的平方而不是线性项原因有两个一是线性惩罚会让优化结果出现频繁的小幅操作类似控制器抖动二是P_bat可正可负线性代价在对称正负取值时存在相互抵消的风险。二次项天然带有平滑功能实测下来储能功率曲线会更干净。第四项是弃光惩罚。光伏弃掉的功率虽然不直接产生成本但从能源利用的角度看浪费了所以在目标函数中乘以一个惩罚系数让算法在“购电”和“弃光”之间做一个权衡。弃光惩罚系数的设置比购电价低一点这样算法优先购电而不是轻易弃光。2.3 约束条件里有几个容易被忽略的细节约束条件决定了优化问题的可行域。第一个关键约束是功率平衡约束等式形式光伏预测出力加上储能功率再加上电网购电功率等于负荷功率加上弃光功率。这里的难点在于储能功率和电网购电功率都是变量如果等式左右两边的正负号处理不统一很容易把模型写反。第二个约束是储能SOC的上下限约束通常设置在0.1到0.9之间。这个约束看起来简单但在MPC滚动窗口中它需要在整个预测时域内的每一步都成立否则未来若干步的状态通道就可能被堵死。第三个约束是储能充放电功率限幅正方向代表放电负方向代表充电。第四个约束是购电功率上限受变压器和公共连接点容量限制。一个容易踩坑的细节是储能不允许同时充电和放电。很多初学者把充放电定义成两个独立变量然后忘记加互斥约束结果优化算法只要有利可图就会让两个变量同时非零充电和放电流在模型里互相抵消实际工程中是完全不可行的状态。我在这个项目里用了一个更巧妙的方式只定义一个符号可变的储能功率变量正值放电、负值充电从变量定义层面就杜绝了同时充放的可能代价是充放电效率只能做对称近似处理。这种做法在项目验证阶段完全够用如果要上高精度仿真再引入分段线性或二进制变量。2.4 关键参数取值与单位一致性参数设置看起来简单但单位不一致会导致整个仿真结果毫无意义。我的采样时间间隔dt是15分钟即0.25小时一天96个时段。储能容量设为1000kWh最大功率250kWSOC范围为0.1到0.9充放电效率设为0.95。这里的单位换算要特别注意功率的单位是kW乘以时间间隔的小时数得到的才是kWh。所以任何出现在目标函数或SOC动态方程里的功率变量都要乘以dt/60才能换算成能量。我一开始在这个换算上吃过亏忘了除以60结果一天的总购电成本数值整整放大了4倍整个调度的经济性完全失真。建议在代码一开始就把dt直接用小时定义比如dt_hour 15/60后续所有乘时间的地方都用dt_hour能少踩很多坑。3. MPC核心逻辑预测、滚动、反馈的三部曲3.1 预测模型怎么构造在MPC框架里预测模型的作用是在当前时刻k根据已知的初始状态和未来输入推演出未来一段时间的系统行为。对微电网这个对象来说系统状态就是储能SOC控制输入是储能功率和购电功率外部输入是光伏出力和负荷需求。在仿真项目中最简单的预测模型构造方式就是假设在预测时域内光伏和负荷的预测数据是已知序列。实际工程里这个序列来自超短期预测系统比如光照辐射预测和负荷预测算法。我这个项目为了先验证调度逻辑仿真里采用了两种预测策略一种是完美预测直接使用真实数据作为预测值另一种是带误差预测在真实数据上叠加高斯噪声用来模拟预测不准的实际情况。SOC的预测等式采用统一效率近似下一时刻SOC等于当前SOC减去储能功率乘以时间间隔再除以电池容量。这里还要考虑效率我采用充放电对称的近似效率0.95充电时储能功率为负、SOC上升放电时功率为正、SOC下降。3.2 滚动优化窗口的数学形式在每个采样时刻k系统需要求解一个有限时域的二次规划或带约束优化问题。决策变量是在预测窗口Np内的全部控制序列包括每个时刻的储能功率、购电功率和弃光功率再加上窗口内的SOC状态变量。目标函数就是前面设计的四项成本的加总约束条件在窗口内的每个时段时间步上都要写一遍。这里有个关键参数预测时域Np的长度。时间步长15分钟如果Np取12等于一次优化看未来3个小时Np取24等于看未来6个小时。窗口长度越长算法对全局趋势的把握越好但预测数据的失准概率也随之增加求解耗时也会变长。我在项目里经过对比最终选择Np12作为默认值在预测精度和求解速度之间是比较平衡的。控制时域Nc可以取小于Np意味着只优化前Nc步的控制量后面保持默认策略。对微电网储能控制来说储能功率本身就是快变量没必要牺牲灵活性所以取NcNp即可简化实现。3.3 反馈校正在这里扮演什么角色滚动优化和反馈校正是MPC区别于一次开环优化的两个核心机制。每次求解完带预测的优化问题后系统只执行最优控制序列的第一步也就是当前时刻的储能功率和购电功率。到下一个采样时刻重新读取实际SOC同时更新预测数据然后重新求解一个新的优化问题。反馈校正在微电网里特别重要原因是光伏预测误差是不可避免的。如果系统按开环计划死板执行一旦实际光伏比自己预测的低储能可能更快放空而MPC的反馈环节会在每一步纠正这个偏差相当于给系统加了一个随时修正的闭环。实际仿真中MPC在预测误差较大的场景下依然能维持SOC不越限而开环调度则可能出现明显偏差。3.4 终端惩罚一个容易被忽略的收尾细节不做终端处理的MPC容易出现一个很尴尬的现象在每个滚动窗口的最后几个时刻优化算法会倾向于把储能用到边界上因为窗口外的事情它不管储能剩余电量对当前窗口的代价没有贡献。结果就是储能经常贴着SOC下限运行整体运行状态很不健康。解决办法有两种。一种是在目标函数里增加终端代价对窗口末尾的SOC偏离参考值进行惩罚等于让算法为窗口结束后留一点余量。另一种是像我项目里那样在滚动窗口内的每一步都引入SOC参考值惩罚并适当调高惩罚系数这样不仅终端状态不会太差整个窗口内的SOC轨迹也更有秩序。实测下来加了SOC参考惩罚之后储能SOC曲线明显更居中调度结果也更符合电池管理的需求。4. Matlab代码实现从参数初始化到滚动循环4.1 参数初始化与预测数据准备Matlab代码的第一部分就是参数初始化。我会把所有关键参数集中放在一个脚本里便于之后统一调整。% 基础时间参数 dt_min 15; % 采样间隔分钟 dt_hour dt_min / 60; % 采样间隔小时 T 96; % 一天的总时段数 Np 12; % MPCC预测时域 % 储能参数 Cap 1000; % 储能容量kWh SOC_min 0.1; SOC_max 0.9; SOC_ref 0.5; eta 0.95; % 充放电对称效率 P_bat_max 250; % 储能最大功率kW % 电网和成本参数 P_grid_max 500; % 购电功率上限kW price zeros(T, 1); price(1:32) 0.3; % 00:00-08:00 谷时 price(33:64) 0.6; % 08:00-16:00 平时 price(65:88) 1.0; % 16:00-22:00 峰时 price(89:96) 0.6; % 22:00-24:00 平时 % 权重系数 lambda_soc 150; % SOC参考值惩罚系数 lambda_bat 0.0002; % 储能功率二次项惩罚系数 lambda_curtail 0.8; % 弃光惩罚系数元/kWh光伏和负荷的预测数据我建议外部读入CSV或者Excel文件。如果只是为了验证算法逻辑也可以在代码内部生成一组符合趋势的模拟数据。光伏出力用一个半正弦型曲线模拟负荷则设置成早晚双峰。为了模拟真实预测误差后续对比分析时给数据加高斯白噪声。4.2 用Yalmip建模目标函数和约束Yalmip是Matlab平台非常成熟的优化建模工具它能让我们用贴近数学表达式的语法建立决策变量、约束和目标函数然后交给底层求解器计算。我在本项目中使用的都是连续变量所以最终呈现的是一个二次规划问题。在初始化阶段我把预测窗口内需要用到的变量定义成向量形式储能功率P_bat、购电功率P_grid、弃光功率P_curtail以及SOC状态变量序列。每个变量都是一个长度为Np或Np1的向量SOC多一个元素是因为既要包含初始状态也要包含预测终点状态。P_bat sdpvar(Np, 1); P_grid sdpvar(Np, 1); P_curtail sdpvar(Np, 1); SOC sdpvar(Np 1, 1);约束建模的时候把窗口内每一步的约束都循环写进去。功率平衡约束、SOC上下限约束、功率限幅等都会按索引逐一添加。这里有一个写代码的细节每次调用MPC的滚动优化时SOC的第一个值绑定外部传入的当前实际SOC这样才能保证优化结果是从真实状态出发的。Constraints []; for j 1:Np % 功率平衡约束 Constraints [Constraints, P_pv_pred(j) P_bat(j) P_grid(j) P_load_pred(j) P_curtail(j)]; % 储能功率限幅 Constraints [Constraints, -P_bat_max P_bat(j) P_bat_max]; % 购电功率限幅 Constraints [Constraints, 0 P_grid(j) P_grid_max]; % 弃光功率非负 Constraints [Constraints, P_curtail(j) 0]; % SOC动态与上下限 Constraints [Constraints, SOC(j1) SOC(j) - (P_bat(j) / eta / Cap) * dt_hour]; Constraints [Constraints, SOC_min SOC(j1) SOC_max]; end Constraints [Constraints, SOC(1) SOC_now];目标函数则是把四项代价写成求和形式注意购电网功率乘上电价和时间步长得到当天该时段的购电费用SOC惩罚作用于SOC与参考值的差值平方储能功率二次惩罚作用于功率平方弃光惩罚作用于弃光功率乘时间步长。Objective sum(price(idx) .* P_grid * dt_hour) ... sum(lambda_soc * (SOC - SOC_ref).^2) ... sum(lambda_bat * P_bat.^2) ... sum(lambda_curtail * P_curtail * dt_hour);4.3 MPC主循环控制逻辑主循环是整个代码最核心的骨架。从k1到T-Np每一步都基于当前时刻SOC和对应预测数据重新建模并求解优化问题然后只取第一步控制量执行最后更新真实SOC并推进到下一个时刻。SOC_true(1) 0.5; P_bat_record zeros(T, 1); P_grid_record zeros(T, 1); P_curtail_record zeros(T, 1); SOC_record zeros(T, 1); ops sdpsettings(solver, quadprog, verbose, 0); for k 1:T-Np idx k:kNp-1; % 取当前窗口内的预测值可在此叠加噪声模拟预测误差 P_pv_pred P_pv(idx); P_load_pred P_load(idx); SOC_now SOC_true(k); % 建模与求解代码同4.2此处省略重复定义 optimize(Constraints, Objective, ops); % 取第一步控制量 P_bat_record(k) value(P_bat(1)); P_grid_record(k) value(P_grid(1)); P_curtail_record(k) value(P_curtail(1)); % 更新真实SOC SOC_true(k1) SOC_true(k) - (P_bat_record(k) / eta / Cap) * dt_hour; end这个循环里面有几个值得注意的工程细节。第一个细节是每次循环都要重新定义变量和约束不能在外部复用同一个sdpvar对象否则求解器会报维度错误。第二个细节是indexing预测窗口取值必须用当前时刻k作为起点避免取到未来窗口外部的数据。第三个细节是只应用第一步的控制量之后真实SOC的推进用的是实际变量而不是优化解里面的SOC序列这正好体现了MPC“反馈校正”的思想优化模型和真实系统之间存在差异每次都从真实状态重新开始。4.4 结果可视化与数据导出调度做完之后没有图形化结果几乎没有办法判断算法表现。我的可视化脚本至少包含三个图形储能功率曲线、电网购电功率曲线和光伏出力曲线叠在一张图上SOC随时间变化的曲线日内电价阶梯图。这三张图互相配合能直观看出调度策略是否合理。figure; subplot(3,1,1); plot(t, P_pv, g, t, P_load, b, t, P_grid_record, r); legend(光伏出力,负荷需求,购电功率); subplot(3,1,2); plot(t, P_bat_record); legend(储能功率(正放电/负充电)); subplot(3,1,3); plot(t, SOC_true); legend(SOC);数据导出我一般用writematrix或者writetable把每天的功率序列、SOC序列和成本累计值保存成CSV文件方便后续做多场景对比和论文绘图。要注意的是实际SOC序列的最后一个值可能是空的因为循环只跑到T-Np后面的时段是“预测尾部”绘图时需要把这一段特殊处理。5. 仿真结果分析MPC和开环优化的真实差距5.1 典型日场景下调度曲线怎么看先说典型场景。光伏从早上6点开始爬坡中午12点达到峰值600kW傍晚18点逐渐降到零。负荷呈现早高峰和晚高峰峰值大约480kW夜间低谷约200kW。购电电价在16点到22点是峰时。MPC调度在这组数据下的表现符合预期夜间谷时电价阶段储能处于轻微充电状态电网购电承担负荷白天光伏大发阶段光伏除了直接供负荷多余电量优先给储能充电SOC一路上升傍晚光伏衰减、电价进入峰时储能主动放电来替代高价购电购电功率明显下降。SOC曲线呈现一个白天爬升、傍晚下降的“驼峰”形态这正是储能套利行为的宏观体现。看这张曲线图的时候重点要看三个地方储能是否在谷时充电、峰时放电购电功率是否在峰时段被压到较低水平SOC是否始终保持在上下限内部。如果这三个点都满足说明目标函数和约束设计基本正确。5.2 开环优化在预测误差下的翻车现场为了对比MPC的优势我做了一组测试同时在预测数据上叠加10%的高斯噪声偏差分别让开环优化和MPC执行。开环优化相当于最早按照预测数据算一次全天计划后面按计划执行MPC则是按照4.3的循环每步重新计算。结果显示开环调度在预测偏差累积之下某些时段储能实际SOC跌到了下限以下。原因不难理解开环计划认为光伏出力充足所以安排储能深度放电但实际光伏出力低于预测储能放过了头。而MPC在滚动过程中逐次修正每一步都知道当前真实SOC能够提前降低放电功率最终SOC依然维持在0.1以上。更有意思的是供电路径上的差异。开环调度出现了一些时段购电功率超过上限的情况因为实际负荷高于预测而计划又没预留足够余量MPC则在每个步长求解时都把购电上限作为硬约束放进去因此没有发生越限。5.3 量化对比表格与指标解读除了看曲线数据化对比更能说明问题。我在同一典型日数据上跑了三种配置开环优化完美预测、开环优化预测有误差、MPC预测有误差得到一组对比数据。需要说明的是这组数据只是特定场景下的仿真结果用来体现趋势不代表所有场景。场景总购电成本元弃光率%SOC越限次数求解耗时开环优化完美预测21204.802.1秒开环优化预测有误差251613.542.1秒MPC预测有误差22545.90每步0.6秒三个结论很直观。第一预测误差让开环优化的总成本上涨了大约18%而MPC只上涨了6%左右这6%主要来自预测误差造成的非最优分配属于理论下限。第二开环优化的弃光率在预测误差下明显飙升MPC则把弃光率维持在接近完美预测的水平。第三MPC每步求解只有0.6秒一天96步总计约58秒虽然比开环的一次性求解慢但远低于实时控制可接受的响应时间完全可以用在在线调度场景。6. 常见问题与排查技巧实录6.1 求解无解的问题排查我在调试过程中遇到最多的问题就是optimize返回无解。MPC滚动优化无解通常不是求解器的问题而是约束集合本身为空也就是不存在让所有约束同时成立的解。排查顺序有讲究。第一步检查功率平衡等式是否和变量方向一致最容易犯的错误是模型里P_bat正负号写反导致等式两边永远无法配平。第二步检查SOC初始值和约束是否矛盾比如SOC初始0.95但设置了SOC_max 0.9当然无解。第三步检查不等式约束有没有互相冲突比如购电上限设得太小而光伏为零、储能又没电不可能满足负荷需求必然无解。工程上更稳妥的防线是加入松弛变量。在功率平衡约束右边加一个小的松弛项并赋予很高的惩罚权重这样即使模型在某一步出现微小不一致求解器也能给出一个近似可行解而不是直接报错代价仅是目标函数略有抬高。实测下来这个技巧能显著降低滚动循环中途跑飞的概率。6.2 储能SOC曲线震荡怎么办换上某个天气突变场景时我注意到储能SOC曲线出现持续的小幅震荡一会儿充电一会儿放电非常频繁。原因是目标函数里SOC参考值惩罚系数lambda_soc太小优化算法觉得SOC偏离参考值无所谓于是只盯着购电成本的最小化而电价分成阶梯以后微小的时间偏移就可能导致储能功率正负来回切换。处理办法有两种。一是提高lambda_soc把SOC尽量稳定在参考值附近波动自然减小二是增加储能功率变化量的惩罚项约束相邻两个时刻的P_bat之差不要太大。第二种方法更对症因为它直接限制了控制动作的剧烈程度等价于限制了储能功率的斜率。注意不要加得太狠否则储能响应速度会变慢出现“该充不充、该放不放”的迟钝现象。6.3 同时买电卖电的“洗澡水困局”如果目标函数里既定义购电功率也定义卖电功率而两者又用不同的电价同时没有互斥约束优化算法就能通过“先买电、再卖电”的方式在模型里无限套利这就是典型的洗澡水困局花小钱买电充入储能再高价卖电一进一出白赚差价而且还会出现母线功率方向的矛盾。这里有一个实用的开发建议项目早期阶段可先关闭卖电通道也就是只定义购电功率非负变量不允许反送功率。这样不仅避免了互斥约束导致的MILP求解难度也简化了代码逻辑。等基础调度逻辑验证完成后再考虑引入二进制变量做买卖互斥约束扩展成更完整的双向功率模型。6.4 求解速度慢的优化方向滚动循环里最拖速度的操作是每步调用Yalmip重新建模。虽然看起来只是又组装了一次约束但在大Np下Yalmip的建模开销会非常可观。实测Np12时每步总耗时约0.6秒其中Yalmip建模占了将近一半真正的求解时间不到0.3秒。如果想提速有几个方向可以尝试。一是减少Np如果光伏和负荷波动不大Np8就能保持不错的性能。二是关闭求解器的实时输出把verbose设为0能省一点IO时间。三是在同一循环内复用变量空间尽量减少sdpvar的重建次数但要注意正确性优先。到了需要实时部署的阶段还可以把Yalmip建模替换成直接调用底层求解器的Matlab接口速度能进一步缩短到毫秒级。做这个项目最大的感受是MPC在微电网调度里的价值不在于预测模型做得有多精确而在于它把“不确定性”从一个需要提前消除的难题变成了一个在每一步都可以修正的小偏差。每一次滚动优化虽然只往前看几步但这几步是基于真实系统状态推算出来的因此整个调度过程始终在正轨上。代码跑通之后我的下一步计划是往模型里加入储能寿命衰减的多目标代价以及多微电网之间的分布式协调那会是一个更有意思的扩展方向而目前这套MPC框架怎么样都能复用到新的模型上这也是我建议你先把它跑通的原因。