
做微电网调度项目这几年我最大的感受是传统日前调度方案在“预测不准”的现实世界里往往会暴露出各种问题——光伏预测一偏储能该充的时候没充该放的时候没放弃光率和购电成本双双上涨。后来我把调度框架换成模型预测控制MPC用滚动优化的方式把“调度”变成了“一边预测、一边修正、只执行第一步”的动态过程效果立竿见影。这篇文章就把我在Python里实现MPC微电网调度优化的完整思路、建模过程、代码结构和踩坑记录整理出来给正在做微电网调度、能源管理系统或者储能优化控制的朋友一个可直接复用的参考。内容覆盖从MPC原理到求解器选型、从目标函数设计到实测排错无论你是刚入门的研究生还是已经在做工程系统的工程师都能在这里找到能直接“抄作业”的东西。1. 微电网调度为什么需要MPC一个被预测误差逼出来的选择1.1 传统调度方案的死穴在哪里微电网调度优化本质上是在满足负荷需求的前提下决定光伏、风电、储能、柴油发电机、电网交互功率各自出力多少让运行成本最低、可再生能源消纳率最高、系统运行最安全。这个问题的经典解法是“日前调度”——提前一天基于第二天的负荷预测和新能源出力预测一次性求解未来24小时通常以15分钟或1小时为步长的机组组合与功率分配方案。听起来很完美但实际运行中会遇到一个绕不开的问题预测永远是有误差的。光伏功率受云层遮挡影响可能几分钟内从80%出力掉到20%负荷预测虽然相对平稳但在极端天气、节假日、重大活动场景下同样会偏离。一旦实际运行曲线和日前预测曲线发生偏差日前调度给出的“最优解”就不再是最优甚至会违反功率平衡、导致储能越限。举个我实测过的例子。某天傍晚光伏预测出力较大日前调度安排储能从15:00开始充电计划在17:00充满。结果当天下午云层突然增多光伏实际出力比预测低了30%储能不但没有充满反而因为电网交互功率受限晚间负荷高峰时段无电可放最后只能高价购电。这在工程上叫“开环调度”的缺陷——决策只做一次没有反馈修正。1.2 MPC到底改了什么MPC的核心思想非常朴素不要一次把未来24小时全部定死而是在每个采样时刻基于当前最新状态和最新预测滚动求解未来一个有限时域内的优化问题然后只执行第一个时刻的控制指令。下一个采样时刻到来时用实测状态刷新预测和问题再求解、再执行第一步如此循环往复。这个看似简单的改动实际效果天差地别。以储能为例如果10:00发现光伏实际出力高于预测MPC会在下一次滚动时立刻调整计划让储能多充一点如果14:00发现负荷飙升MPC又会重新分配让储能提前进入放电状态。相当于系统每隔一段时间就“重新睁眼看看世界”而传统日前调度是“闭着眼睛按计划跑一天”。从控制理论角度说MPC把调度问题从“离线优化”变成了“在线优化”用反馈弥补预测的不确定性。这也是为什么MPC在工业过程控制、自动驾驶、飞行控制等领域能大规模落地——它天生就是一个“对抗不确定性”的框架。用在微电网这种新能源占比高、波动性强的场景几乎是为它量身定做的。1.3 MPC、规则策略和日前调度的直观对比为了更直观地看差距我把三种典型微电网调度策略放在一起对比对比维度日前调度实时规则策略MPC滚动调度决策频率一天一次实时响应每个采样周期一次预测利用依赖24h预测基本不依赖依赖短期预测并滚动刷新约束处理硬约束求解靠规则硬编码显式建模支持硬/软约束应对预测误差差开环无修正反应快但难做到最优滚动修正鲁棒性强计算复杂度低极低中等需在线求解经济性表现实测基准值比基准高约8%15%通常比基准低5%12%规则策略比如“峰时放电、谷时充电”的固定策略虽然响应快但很难同时兼顾多个约束和时变电价更没法提前为未来几小时的光伏波动做准备所以我一直认为微电网调度往MPC方向走是性价比最高的选择既保留优化能力又具备实时反馈能力。2. MPC调度问题的建模先把成本讲清楚再谈控制2.1 预测模型、滚动优化、反馈校正三件套任何一个MPC系统都包含三个核心模块预测模型、滚动优化和反馈校正。我在微电网调度代码里对齐的结构如下预测模型用一个状态空间模型描述微电网的动态行为核心是储能系统的SOC荷电状态递推方程以及光伏、负荷的短期预测序列。滚动优化在每个采样时刻求解一个有限时域内的优化问题。目标函数包含购电费用、弃光惩罚、储能退化成本等约束包含功率平衡、SOC上下限、功率爬坡限制等。反馈校正每个采样时刻开始时用实际采集的SOC、实际功率数据修正模型初值再做下一轮优化。这相当于把“模型误差”和“预测误差”拉回正轨。预测模型不一定要非常复杂。在工程实践中我通常把光伏和负荷预测作为一个“外部输入序列”用时间序列模型或者数值天气预报生成而把储能动态建模为线性差分方程。重点是把能量关系写对——这部分直接决定了滚动优化能不能收敛到正确结果。2.2 目标函数怎么设计成本、弃光、电池损耗的加权博弈目标函数是MPC的核心。我在项目中用过多种形式最终主推的版本是把三个目标加权求和运行成本最小包括从电网购电费用、柴油机燃料费用弃光惩罚是为了鼓励消纳新能源当光伏出力超过可消纳能力时产生惩罚项储能退化成本则用充放电功率的线性或二次项近似电池循环寿命损耗。一个典型的目标函数写出来是这样[ \min \sum_{k0}^{N-1} \left( C_{\text{grid}}(k) \cdot P_{\text{grid}}(k) \alpha \cdot P_{\text{curtail}}(k) \beta \cdot |P_{\text{ess}}(k)| \right) ]里面三个参数非常重要( C_{\text{grid}}(k) )是分时电价体现“峰时买电贵、谷时买电便宜”( \alpha )是弃光惩罚系数设置过低会导致系统宁可弃光也不储能设置过高又可能导致储能过度充放电( \beta )是储能损耗系数如果设得太大储能会“懒得动”整个调节能力下降。我调试项目时会先让( \alpha )和( \beta )取一个“感觉合理”的值跑通代码然后做敏感性分析把某个系数放大10倍、缩小到0.1倍观察SOC曲线和成本变化。经验是弃光惩罚系数通常设为电价的1.52倍储能退化系数则按电池每kWh循环成本的1/1000来粗估这样目标函数的量纲比较接近求解器也不容易病态。2.3 约束条件功率平衡、SOC动态、不可行性的兜底处理约束是MPC最容易被坑的地方。我最常用的约束集如下功率平衡约束光伏出力 风电出力 储能放电 电网购电 柴油机出力 负荷 储能充电 弃光功率。这是一个等式约束如果写不平MPC几乎必然无解。SOC动态约束( SOC(k1) SOC(k) - \eta_c P_{\text{ch}}(k) \Delta T / E_{\text{cap}} P_{\text{dis}}(k) \Delta T / (\eta_d E_{\text{cap}}) )。充放电效率分开处理且充放电不能同时为正——这个互斥约束在标准MPC里可以用两变量非负互补约束近似。SOC上下限约束一般取0.10.9避免深度充放电伤害电池。爬坡约束储能和柴油机功率变化速率有限防止指令跳变。联络线容量约束微电网与配电网的交互功率有上限。在项目里最常出问题的其实是等式约束和多个不等式约束同时收紧时模型会无解infeasible。比如SOC刚好在下限、又要求必须放电那功率平衡就必然被打破。处理办法有两个一是把硬约束改成“软约束”即引入松弛变量在目标函数里加上对松弛变量的惩罚二是检查约束的物理含义避免矛盾条件。我的代码结构里松弛变量的设置是保留项目每次必加否则跑仿真中途解不出来会让你怀疑人生。3. Python代码如何落地从数学公式到可运行程序3.1 求解器选型为什么我最后选了cvxpy OSQPPython里能求解优化问题的库很多但要适配MPC这种“重复求解小规模优化问题”的场景需要认真挑一下。我对比过几个方案求解方案适用问题类型优点缺点scipy.optimize.minimize非线性/通用优化上手容易文档多慢约束多的调度问题容易不收敛pulp线性规划/混合整数经典LP求解方便不支持二次项MPC目标函数难扩展cvxpy OSQP凸优化尤其QP建模语法简洁OSQP求解快支持在线重复求解需要理解凸优化基本概念Gekko动态优化/MPC内置MPC功能偏过程控制自定义调度场景时要绕很多弯最终我选cvxpy OSQP组合。原因有三第一微电网调度目标函数里储能损耗项用二次项更平滑正好落在QP二次规划的框架里第二cvxpy的约束建模语法接近数学公式代码可读性好后续维护方便第三OSQP是专门为嵌入式、在线优化设计的求解器求解毫秒级小规模问题毫无压力非常适合MPC滚动循环里反复调用。3.2 仿真环境与数据准备没有真实数据怎么跑通MPC很多朋友在初学阶段没有真实微电网数据会卡在“数据从哪来”这一步。我的建议是不要等数据先用构造数据把框架跑起来。具体做法分三步第一步构造典型日光伏出力曲线。用一个简单的正弦型曲线叠加随机噪声模拟晴天光伏正午出力最高、早晚为零再设定一个“多云扰动”场景在某个时段把出力人为压低30%。第二步构造负荷曲线。典型居民/商业混合负荷是早晚两个高峰合成一个双峰曲线再加白噪声表示随机波动。第三步定义分时电价。峰段8:00-11:00、18:00-21:00电价高平段和谷段电价低这个阶梯结构直接决定了储能“低充高放”的策略是否会被MPC自动学会。有了这三样数据层就齐了。注意在仿真代码里把数据封装成一个DataFrame或NumPy数组即可关键在于采样时间( \Delta T )和总仿真时长的设置——我常用( \Delta T 15 )分钟仿真一天即96个采样点。这里插入我自己画的一个逻辑闭环图描述非图形预测模块生成未来N步的PV和负荷序列 → 优化器根据预测和当前状态求解出M步控制指令 → 执行器只取第一个指令作用到系统 → 状态观测器更新SOC等状态 → 下一时刻重复。这个循环就是MPC的“灵魂”。3.3 核心代码逻辑拆解一步步把MPC写出来下面给出一个简化可运行的MPC微电网调度核心代码框架。注意这不是完整工程代码而是帮你理解“每一步在干什么”。import numpy as np import cvxpy as cp # ---------- 基础参数 ---------- N 8 # 预测时域8步即未来2小时步长15分钟 E_cap 500.0 # 储能容量 kWh SOC_min, SOC_max 0.1, 0.9 eta_ch, eta_dis 0.95, 0.95 P_ch_max 100.0 # 最大充电功率 P_dis_max 100.0 # 最大放电功率 P_grid_max 200.0 # 联络线最大交互功率 alpha 0.5 # 弃光惩罚系数/电价基准 beta 0.02 # 储能损耗系数 # 预测序列用简单函数生成实际应从预测模块读取 pv_pred np.array([120, 110, 90, 80, 70, 60, 50, 40]) load_pred np.array([80, 85, 90, 100, 110, 120, 130, 140]) price np.array([0.5, 0.5, 0.8, 0.8, 1.0, 1.0, 1.2, 1.2]) def mpc_step(soc_now, pv_pred, load_pred, price): # 决策变量 P_ch cp.Variable(N) # 储能充电功率 P_dis cp.Variable(N) # 储能放电功率 P_grid cp.Variable(N) # 电网交互功率正为购电 P_curtail cp.Variable(N, nonnegTrue) # 弃光功率 SOC cp.Variable(N 1) # SOC动态序列 # 目标函数购电成本 弃光惩罚 储能损耗 cost 0 for k in range(N): cost price[k] * P_grid[k] * 0.25 # 0.25小时 15分钟 cost alpha * P_curtail[k] * 0.25 cost beta * (P_ch[k] P_dis[k]) * 0.25 # 约束集合 constraints [] constraints [SOC[0] soc_now] constraints [SOC[k1] SOC[k] eta_ch * P_ch[k] * 0.25 / E_cap - P_dis[k] * 0.25 / (eta_dis * E_cap) for k in range(N)] constraints [SOC_min SOC[k] SOC_max for k in range(N1)] constraints [0 P_ch[k] P_ch_max for k in range(N)] constraints [0 P_dis[k] P_dis_max for k in range(N)] constraints [-P_grid_max P_grid[k] P_grid_max for k in range(N)] # 功率平衡关键等式 for k in range(N): constraints [pv_pred[k] P_dis[k] P_grid[k] load_pred[k] P_ch[k] P_curtail[k]] problem cp.Problem(cp.Minimize(cost), constraints) problem.solve(solvercp.OSQP, verboseFalse) if problem.status optimal: return P_ch.value, P_dis.value, P_grid.value, SOC.value else: return None, None, None, None这段代码的核心逻辑是每个控制周期把未来N步的预测数据和当前SOC传给mpc_stepcvxpy构建优化问题并调用OSQP求解然后从解中取第一个时刻的P_ch[0]、P_dis[0]、P_grid[0]作为实际执行指令。注意SOC递推约束中充电用效率放大能量输入、放电用效率折算能量输出的写法这是很多初学者容易搞反的地方——搞反了SOC仿真曲线会越跑越离谱。3.4 滚动循环与结果可视化仿真96步的真实代码套路拿到单步MPC函数之后剩下的就是把它放进滚动循环。标准写法soc 0.5 soc_history [soc] grid_history [] pv_curtail_history [] ch_history [] dis_history [] for t in range(96): # 一天96个15分钟 # 获取t时刻之后的预测序列实际工程中由预测模块实时产生 pv_seq get_pv_forecast(t, N) # 从t开始取N步 load_seq get_load_forecast(t, N) price_seq get_price_forecast(t, N) # 求解MPC P_ch, P_dis, P_grid, SOC_seq mpc_step(soc, pv_seq, load_seq, price_seq) if P_ch is None: break # 求解失败处理 # 只执行第一步 p_ch_act P_ch[0] p_dis_act P_dis[0] p_grid_act P_grid[0] # 更新SOC用执行值而不是预测值 soc soc eta_ch * p_ch_act * 0.25 / E_cap - p_dis_act * 0.25 / (eta_dis * E_cap) soc np.clip(soc, SOC_min, SOC_max) # 记录 soc_history.append(soc) grid_history.append(p_grid_act) ch_history.append(p_ch_act) dis_history.append(p_dis_act)滚动循环的关键细节是更新SOC时必须使用第一步的实际执行功率而不是预测序列里的值。很多人第一次写MPC代码就是用SOC_seq[1]来更新状态这在预测与执行完全一致时没问题但只要预测有误差SOC就会逐渐漂移跑几个小时之后仿真就崩了。这个坑我踩过不止一次后面专门列一节细说。跑完96个点后用Matplotlib把SOC曲线、购电功率曲线、弃光功率曲线和电价曲线画在同一个图上能非常直观地看到MPC是否学会了“谷充峰放”策略。我在项目里有几次调完参看到储能曲线自动跟着电价峰谷走那种感觉确实舒坦。4. 实测中的五个典型坑与排查技巧4.1 优化无解先查约束“打架”再加松弛变量MPC求解返回problem.status不是optimal可能是infeasible。我遇到的最常见原因是储能SOC约束和功率平衡约束同时收紧时互相冲突。比如SOC已经到0.1下限但约束还要求它继续放电来平衡功率那自然无解。排查顺序先打印求解状态再逐条检查约束。最快捷的办法是把SOC下限临时改成0看问题是否变可解——如果变了说明是储能容量不够支撑某个时段放电需求。彻底解决是在约束中加入松弛变量概念上就是“允许功率平衡有小幅偏差但偏差越大约罚越多”slack cp.Variable(N, nonnegTrue) constraints [pv_pred[k] P_dis[k] P_grid[k] load_pred[k] P_ch[k] P_curtail[k] slack[k] - slack2[k]]目标函数里给slack一个很大的惩罚系数比如100倍电价。实际调度中系统几乎不会触发松弛但在极端场景下它保证优化器永远有解这对仿真稳定性意义重大。4.2 SOC漂移执行值和预测值混用的后果我在4.3里提过但这里必须再强调一次。MPC的滚动机制是“求解N步、只执行第一步”下一时刻的状态必须基于“第一步实际执行效果”来计算而不是直接取求解器给出的第二步预测。有一个项目里我是这样写错的# 错误写法直接拿求解结果里的SOC序列下一步 soc SOC_seq[1]在理想仿真里这没问题但一旦光伏预测序列更新、或负荷实测值和预测值有偏差这行代码就等于让状态凭空跳变。正确做法永远是通过储能动态方程用第一步的充放电功率重新计算SOC。我建议在代码里把状态更新单独写一个函数只接收功率输入不接收SOC序列从结构上杜绝这类错误。4.3 预测刷新不及时恶劣天气下MPC效果骤降的元凶MPC的反馈校正能力是建立在“每个采样周期拿到最新预测”的基础上。如果预测数据一天只更新一次那MPC本质上退化成“用MPC算法求解日前调度”滚动优势完全丧失。在实际工程系统中光伏预测的更新频率至少要做到15分钟一次负荷预测可以放宽到小时级。我在仿真里为了模拟这个效果会把预测模块的输入设计成“带噪声的真实PV 更新周期参数”。当更新周期设为96即一天一次时MPC成本和平淡的日前调度几乎一样当更新周期设为1每步更新时成本明显下降。这个对比实验做出来很有说服力也是向别人解释MPC价值的好素材。4.4 求解时间失控预测时域不是越长越好MPC性能高度依赖预测时域N。N太短控制器“目光短浅”看不到远期低价充电的机会N太长决策变量和约束数量线性增长OSQP虽然快但也会从几毫秒涨到几百毫秒。实测数据96步仿真、P_ch/P_dis/P_grid共3N个连续决策变量预测时域N平均单步求解时间成本表现相对于基准41小时1.2 ms决策近视效果较差82小时2.5 ms平衡较好推荐164小时6 ms效果更优但边际递减328小时25 ms耗时可接受9624小时120 ms效果接近最优但实时性差我现在的经验是N取816之间既能覆盖储能一个充放周期的大部分信息又不至于拖慢在线计算。当然如果你的系统是真正的实时控制需要50ms以内返回结果还要配合代码优化和求解器热身技巧。4.5 权重系数调参先从“无惩罚”模型跑起多目标函数的权重调参是MPC项目里最费时间的一步。我的独家经验是先把( \alpha )和( \beta )全设为0只看购电成本跑一个纯经济调度。这时候系统会极端化——比如让储能频繁满充满放来赚取峰谷差价但至少求解是稳定的。然后逐步加大( \beta )电池损耗惩罚观察SOC是否不再频繁到达上下边界。( \beta )从一个很小的值比如0.001开始每次乘10直到SOC曲线的波动幅度符合电池寿命管理要求。最后再加( \alpha )弃光惩罚观察弃光率是否降到可接受范围。有个在工程上很实用的判断标准SOC曲线不应该在同一小时内既充到顶又放到底。如果出现这种情况说明储能损耗惩罚太小系统在“过度套利”——真实项目里电池寿命会被这种频繁充放严重影响。5. 从MPC代码到微电网工程落地的经验补充5.1 仿真通过之后距离工程部署还差什么很多实验室项目止步于仿真但实际工程部署还有几道坎这里提醒一下第一通信与数据采集延迟。MPC的预测输入来自数据采集与监控系统数据上报频率、网络延迟会直接影响预测的时效性。如果SOC数值滞后超过一个采样周期反馈校正就会失效。我处理过的一个项目里电池管理系统上报SOC延迟达到2分钟导致MPC一直拿旧状态做决策储能充放电行为明显“慢半拍”。解决思路是在状态估计模块加一个简单的一阶滤波或预测修正。第二模型失配。仿真里储能的充放电效率、容量都是固定值真实电池的效率会随温度和电流变化SOC估算本身也有误差。工程做法是在MPC之外加一个状态估计层比如卡尔曼滤波把SOC估计值而不是直接测量值送给MPC。第三安全逻辑冗余。MPC的指令在工程上不能直接下发到储能变流器中间还必须有一个保护逻辑层做功率限制、急停、通讯超时处理等。否则MPC的某个指令异常比如P_ch为负的数值bug会导致硬件报警甚至故障。我的原则是MPC负责“怎么省钱”保护逻辑负责“绝对不能出事”。5.2 一个可以立刻上手的扩展方向多时间尺度协同调度如果你已经把单层MPC跑通了我推荐往“多时间尺度”方向扩展。思路是用日前调度或长时间尺度MPC小时级决定储能的整体充放电趋势用短时间尺度MPC分钟级在趋势框架内做精细化修正。这种分层结构在工业界非常常见既能利用长时间尺度信息做全局优化又能在短时间尺度应对突发波动。实现方式也不复杂——把长尺度MPC的SOC轨迹作为一个参考约束加入短尺度MPC的目标函数让它“尽量跟着参考走但允许偏差”。我测试过在光伏波动剧烈的场景下分层MPC比单层MPC成本再降低3%7%而且储能动作更加平滑。5.3 调试MPC代码的几条心得最后分享几个实打实的调试心得先跑通再优化。我见过太多人一上来就追求复杂的预测模型、多目标权重、非线性约束结果代码跑不通排查困难。正确做法是先线性化、先不加惩罚项、先跑短时域确保滚动循环能转起来再逐渐增加复杂度。注重可视化。MPC调试时我习惯开三个图第一个是功率分配图光伏、储能、电网、负荷四条曲线第二个是SOC曲线第三个是购电成本累计。三个图放在一起任何异常比如储能反向充电、SOC跳变、成本突增都能一眼定位。没有可视化你只能对着密密麻麻的数字发呆。把预测误差也作为仿真的一部分。纯理想预测下MPC和理论上限对比没有差距无法体现MPC的滚动价值。我在仿真里一定给预测模块加入不同程度的高斯噪声故意模拟“预测不准”。这样跑出来的对比数据才有说服力——MPC相对日前调度的成本下降主要来自对预测误差的抵抗能力。我做完这个MPC微电网调度项目后的最大体会是MPC不是一个“神奇的算法”而是一套“有反馈机制的优化框架”。它的每一项优势——滚动修正、约束处理、多目标权衡——都是建立在正确的建模、合理的参数和扎实的代码实现之上。如果只把它当成“一个能在线重复求解的优化器”那就浪费了它最核心的反馈修正能力。希望这篇文章能帮你把MPC微电网调度这条路走通少踩几个我当年踩过的坑把更多时间花在真正有意思的调参和策略设计上。