做微电网调度项目这几年,我最大的感受是:传统日前调度方案在“预测不准”的现实世界里,往往会暴露出各种问题——光伏预测一偏,储能该充的时候没充,该放的时候没放,弃光率和购电成本双双上涨。后来我把调度框架换成模型预测控制(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_{k=0}^{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.5~2倍,储能退化系数则按电池每kWh循环成本的1/1000来粗估,这样目标函数的量纲比较接近,求解器也不容易病态。
2.3 约束条件:功率平衡、SOC动态、不可行性的兜底处理
约束是MPC最容易被坑的地方。我最常用的约束集如下:
- 功率平衡约束:光伏出力 + 风电出力 + 储能放电 + 电网购电 + 柴油机出力 = 负荷 + 储能充电 + 弃光功率。这是一个等式约束,如果写不平,MPC几乎必然无解。
- SOC动态约束:( SOC(k+1) = 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.1~0.9,避免深度充放电伤害电池。
- 爬坡约束:储能和柴油机功率变化速率有限,防止指令跳变。
- 联络线容量约束:微电网与配电网的交互功率有上限。
在项目里最常出问题的,其实是等式约束和多个不等式约束同时收紧时,模型会无解(infeasible)。比如SOC刚好在下限、又要求必须放电,那功率平衡就必然被打破。处理办法有两个:一是把硬约束改成“软约束”,即引入松弛变量,在目标函数里加上对松弛变量的惩罚;二是检查约束的物理含义,避免矛盾条件。我的代码结构里,松弛变量的设置是保留项目,每次必加,否则跑仿真中途解不出来会让你怀疑人生。
3. Python代码如何落地:从数学公式到可运行程序
3.1 求解器选型:为什么我最后选了cvxpy + OSQP
Python里能求解优化问题的库很多,但要适配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, nonneg=True) # 弃光功率 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[k+1] == 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(N+1)] 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(solver=cp.OSQP, verbose=False) 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_step,cvxpy构建优化问题并调用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, nonneg=True) 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 | 平均单步求解时间 | 成本表现(相对于基准) |
|---|---|---|
| 4(1小时) | 1.2 ms | 决策近视,效果较差 |
| 8(2小时) | 2.5 ms | 平衡较好,推荐 |
| 16(4小时) | 6 ms | 效果更优,但边际递减 |
| 32(8小时) | 25 ms | 耗时可接受 |
| 96(24小时) | 120 ms | 效果接近最优但实时性差 |
我现在的经验是,N取8~16之间,既能覆盖储能一个充放周期的大部分信息,又不至于拖慢在线计算。当然,如果你的系统是真正的实时控制(需要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微电网调度这条路走通,少踩几个我当年踩过的坑,把更多时间花在真正有意思的调参和策略设计上。