1. 项目概述
1.1 微电网调度为什么需要MPC:一个被低估的决策问题
做微电网调度的人都知道,这问题表面上是"分配功率",实际上是在跟不确定性打交道。光伏出力一会儿高一会儿低,负荷曲线像心电图一样跳动,电价还会按峰谷波动。传统的线性规划调度方式要求你提前一天把计划排死,结果到了当天实际运行,光伏突然被云遮住,计划里的功率平衡瞬间破功,你只能靠自发性调度去补救,整个优化目标基本作废。
模型预测控制的思路完全不同——它把调度做成一个滚动窗口。每个控制周期,你基于当前系统状态和预测信息,做未来一段时间的优化决策,然后只执行第一步,等下一个周期再重新滚动。这种"边看边走"的模式天然适配微电网的随机性。用MPC做微电网调度优化,本质上就是解决"在预测不准、运行变化快、多个设备耦合约束多的情况下,如何保证经济性与安全性不至于顾此失彼"的问题。
1.2 这个项目解决的是什么问题,适合谁参考
这个项目锁定的是微电网中典型的多设备协同调度场景:光伏、储能电池、柴油发电机、以及负荷需求,在分时电价机制下做经济调度。核心任务是让储能系统在电价低谷充电、高峰放电,同时控制柴油发电机的出力尽量靠近最低运行成本区间,并且保证母线功率平衡和SOC上下限约束。
用一句话概括:当你拥有一堆实时数据(光伏预测、负荷预测、电价曲线),MPC能帮你把"当天应该让电池充多少、放多少,发电机什么时候开、出力多少"这个决策问题,变成一个每15分钟滚动一次的求解问题。
适合参考这类方案的人群包括:做微电网能量管理系统(EMS)、研究可再生能源并网调度、以及在企业里做园区微电网控制算法的工程师。如果你正在研究强化学习调度,MPC也是一个非常好的参照系——它能稳定输出可行解,作为对比基线相当可靠。
1.3 为什么选择Python实现:快速迭代才是真实需求
微电网调度MPC的落地形态有很多,工业界常见的有C++配合QPSolver,或者MATLAB的MPC工具箱。而Python的优势在于工程建模效率和良好的生态衔接。cingular开发环境上,你可以用numpy和scipy管理数组与矩阵,用casadi或cvxpy做凸优化模型,这些都和电力系统的数据格式天然兼容。
这个项目采用Python实现的关键价值在于:将MPC的核心逻辑拆解为可读代码,方便研究者快速验证模型、修改约束、测试不同预测时域效果。相比MATLAB的"黑盒封装",Python代码让你能看到每个矩阵的实际构造过程,这对理解MPC和做二次开发来说价值非常大。
2. MPC控制器的核心设计思路
2.1 预测模型:把微电网运动状态写成一阶差分方程
微电网的MPC设计起点是状态空间模型。我们把储能电池的SOC和上一时刻的SOC、充放电功率、调度周期关联起来:
[ SOC(k+1) = SOC(k) - \frac{\eta_c P_c(k) \Delta t}{C_{bat}} + \frac{P_d(k) \Delta t}{C_{bat}\eta_d} ]
在这个公式里,(P_c) 是充电功率(正数),(P_d) 是放电功率(正数),两个变量在约束上互斥。这个式子说明MPC和传统最优控制的差异:它不直接给出一个固定的控制序列,而是把 (SOC(k+1)) 当作由当前状态和输入共同决定的状态变量,放进预测窗口逐层递推。
负荷与光伏在预测模型里通常当作可测扰动(Measured Disturbance)。它们不参与状态方程的动态递推,但它们会出现在功率平衡等式里:
[ P_{grid}(t) + P_{pv}(t) + P_{gen}(t) - P_c(t) + P_d(t) = P_{load}(t) ]
这个等式非常重要,它把跟踪目标、经济目标和变量耦合全部串到了一起。写代码时你会需要构建一个从k到k+Np的递推矩阵,将SOC在每个预测周期里的变化量表达为决策变量(充放电功率、发电机出力、购电功率)的线性组合。
2.2 为什么要做滚动优化而不是全局最优
传统经济调度是开环的——你求一次全局最优解,然后照着执行。这在理想条件下没问题,但一旦预测偏差发生,开环调度的劣势立刻暴露。比如早上十点光伏预测显示,一个小时后出力为600kW;但实际跑到十点半,云层遮住太阳,出力掉到300kW。开环方案里电池仍然按"600kW光伏"的计划充电,结果电网功率出现缺额,母线电压跌了,储能SOC也被迫深充,整个运行偏离预期。
MPC用滚动优化对抗这种不确定性的方式很像下棋——每步不追求把整盘棋算死,只算未来有限步数,但每走一步都重新计算一次剩余路径。在项目中的体现就是:把时间切成15分钟一个周期,每个周期都重新求解未来4小时(16个步长)的最优调度序列,但只应用第一个步长的决策,到下一个周期再拉一次最新的预测数据,重新求解。这样预测误差每15分钟被修正一次,系统性累积偏差是有限的。
2.3 目标函数的构成:经济性要可量化,惩罚项要有实际物理含义
MPC的目标函数一般写成二次型加线性项的组合:
[ J = \sum_{i=1}^{N_p} || w_1 \cdot (P_{grid}(k+i) - P_{ref}) ||^2 + w_2 \cdot C_{buy}(k+i) P_{grid}(k+i) + w_3 \cdot (P_{gen}(k+i) - P_{gen,min})^2 + w_4 \cdot (SOC(k+i) - SOC_{ref})^2 ]
每一项都有实际意思。第一个二次项让并网点功率尽量平滑,避免对主网造成冲击;第二项是购电成本项,根据分时电价计算;第三项让柴油发电机尽量贴近经济运行点而非满负荷运转;第四项是SOC的参考项,保证电池在日终时回到设定值,避免第二天无电可用。
这里我想强调一个容易踩坑的点:SOC参考项不是越小越好。很多入门者把SOC_ref设成100%,结果MPC会拼命在预测窗口内把电池充满,不顾电价和经济性。更合理的做法是设置一个允许范围,比如40%到90%,SOC_ref取中间值,权重不要过高。这个项目里我用的是时变的SOC参考,在光伏输出高峰时段强制SOC上抬,在后半夜让SOC回落到40%左右。
3. Python代码实现路径
3.1 模块划分与数据流设计
代码实现部分,我没有把所有逻辑塞进一个文件——那样调试起来非常痛苦。整个项目拆成四个相对独立的部分:
mpc_microgrid/ ├── data_loader.py # 加载光伏、负荷、电价数据 ├── model.py # 状态空间模型、约束构造 ├── mpc_controller.py # MPC核心求解逻辑 ├── simulation.py # 闭环仿真主流程 └── plot_results.py # 可视化data_loader做的事情简单直接:从CSV读取时间序列数据,做归一化处理,并切分成预测数据和控制周期数据。model.py里最核心的是构造预测矩阵——这一块是用numpy实现的,先把状态递推矩阵A、B、Bd搭好,再通过循环展开未来Np步的状态表达式。mpc_controller.py是核心求解器,负责调用cvxpy或casadi构造优化问题并求解。simulation.py负责模拟闭环过程:每个周期调用MPC控制器得到首步动作,更新系统状态,再进入下一个控制周期。
3.2 用cvxpy构造MPC优化问题的关键代码
cvxpy是构建凸优化模型非常好用的工具,支持把矩阵形式的二次约束和目标函数直接写上去。下面是最核心的求解函数,这段代码决定了整个MPC的决策质量:
import cvxpy as cp import numpy as np def solve_mpc(soc_current, pv_pred, load_pred, price_pred, horizon=16, dt=0.25, soc_max=0.9, soc_min=0.4, p_grid_max=500.0, battery_power_max=200.0): # 决策变量:购电功率、光伏弃光、储能充电、储能放电、发电机出力 p_grid = cp.Variable(horizon) p_pv_curtail = cp.Variable(horizon) p_ch = cp.Variable(horizon) p_dis = cp.Variable(horizon) p_gen = cp.Variable(horizon) soc = cp.Variable(horizon) # 电池容量 battery_capacity = 1000.0 # kWh charge_eff = 0.95 discharge_eff = 0.92 # 目标函数 cost = 0.0 for k in range(horizon): # 购电成本 cost += price_pred[k] * p_grid[k] * dt # 发电机燃料成本,简化为二次函数 cost += 0.1 * p_gen[k] * p_gen[k] + 0.3 * p_gen[k] # SOC越限惩罚 cost += 0.001 * cp.maximum(0.0, soc[k] - soc_max) ** 2 cost += 0.001 * cp.maximum(0.0, soc_min - soc[k]) ** 2 # 初始SOC状态递推的约束 constraints = [] soc_prev = soc_current for k in range(horizon): # 功率平衡约束 constraints += [ p_grid[k] + pv_pred[k] - p_pv_curtail[k] + p_gen[k] - p_ch[k] + p_dis[k] == load_pred[k] ] # SOC动态 if k == 0: constraints += [ soc[k] == soc_prev + (charge_eff * p_ch[k] - p_dis[k] / discharge_eff) * dt / battery_capacity ] else: constraints += [ soc[k] == soc[k - 1] + (charge_eff * p_ch[k] - p_dis[k] / discharge_eff) * dt / battery_capacity ] # 充放电互斥约束 constraints += [p_ch[k] >= 0, p_dis[k] >= 0, p_ch[k] <= battery_power_max, p_dis[k] <= battery_power_max] # 充电和放电不能同时进行(用松弛线性约束表达) constraints += [p_ch[k] + p_dis[k] <= battery_power_max] # 功率限制 constraints += [p_grid[k] >= 0, p_grid[k] <= p_grid_max] constraints += [p_pv_curtail[k] >= 0, p_pv_curtail[k] <= pv_pred[k]] constraints += [p_gen[k] >= 0, p_gen[k] <= 100.0] constraints += [soc[k] >= soc_min, soc[k] <= soc_max] prob = cp.Problem(cp.Minimize(cost), constraints) prob.solve(solver=cp.OSQP) if prob.status == 'optimal': return { 'p_grid': p_grid.value, 'p_ch': p_ch.value, 'p_dis': p_dis.value, 'p_gen': p_gen.value, 'p_pv_curtail': p_pv_curtail.value, 'soc': soc.value } else: return None这段代码里有一个细节值得解释:充放电互斥约束,我没有用大量二进制变量去强制互斥(那是混合整数规划的玩法),而是采用 (P_c + P_d \leq P_{bat,max}) 这种松弛约束。这个技巧在电池充放电效率不一致的情况下能正常工作,因为MPC求解完,你实际执行时只会执行一个方向,另一个方向的功率在仿真中不起作用。如果你单纯用二进制变量,每个时间步都要加一个麻烦的Big-M约束,求解速度会慢不少。
3.3 滚动窗口与闭环仿真流程
有了单步求解函数后,闭环仿真就是把它放进一个for循环里。以下就是我整个项目仿真主循环的骨架:
def run_simulation(pv_series, load_series, price_series, horizon=16, interval_steps=96): soc = 0.6 # 初始SOC soc_history = [] p_grid_history = [] p_pv_curtail_history = [] for step in range(interval_steps): # 基于当前位置,截取预测数据 pv_pred = pv_series[step:step+horizon] load_pred = load_series[step:step+horizon] price_pred = price_series[step:step+horizon] # 调用MPC求解 result = solve_mpc(soc, pv_pred, load_pred, price_pred, horizon) if result is None: print(f"Step {step}: Optimization failed!") break # 只执行第一步 soc = result['soc'][0] soc_history.append(soc) p_grid_history.append(result['p_grid'][0]) p_pv_curtail_history.append(result['p_pv_curtail'][0]) return soc_history, p_grid_history, p_pv_curtail_history预测窗口的数据截取要注意一个边界问题:仿真推进到时序末尾时,后半段数据长度可能不足horizon。我在实现里用了一个padding策略,把不足部分用最后一个可用值补满,防止索引越界。这种处理在实际工程中很常见——你不能因为数据缺失就让整个MPC停下来。
3.4 电力平衡与约束条件在Python中的实现细节
MPC里约束不是越多越好,而是越贴近物理越好。我最终版本中保留了以下这些约束,每个都有明确的必要理由:
- 功率平衡约束:这是电网运行的第一硬约束,必须精确满足。如果出现不平衡量,要么代表求解器数值精度有问题,要么代表某个自由变量的边界约束不合理。
- SOC上下限约束:防止过充过放延长电池寿命。工程上,SOC区间不是固定的,温度不同的季节可以调整——冬天我会把下限从0.2调高到0.3,减少电池在低温下深度放电的风险。
- 储能功率爬坡约束:这个在初版代码里没加,后来仿真时发现电池从60kW充电直接跳到200kW放电,中间没有过渡,对变流器很不友好。加上 (P(k) - P(k-1) \leq 50kW) 的坡道约束后,运行曲线平滑了很多。
- 光伏弃光变量:很多人一开始不设这个变量,因为觉得"光伏不用白不用"。但在下午光伏过剩时段,没有弃光变量,MPC就会让电池满充,SOC顶到上限;如果SOC提前触顶,功率平衡约束被迫让购电功率出现负数(即倒送电网),这在实际情况中可能不被允许。弃光变量就是给MPC一个"额外泄压阀"。
4. 参数整定与性能优化
4.1 预测时域与控制周期的选择
预测时域((N_p))的选择直接影响系统性能和计算开销。短时域(比如4步/1小时)响应快,但对远期电价信号不敏感,容易做"今天赚小钱、明天亏大钱"的短视决策。长时域(比如48步/12小时)理论上更全局,但预测误差太多,远端数据失真,反而影响当前决策。
这个项目里我最终选了16步(4小时)。原因是:微电网中的电价变化周期一般是每日两个峰、两个谷,4小时的预测窗口能覆盖到至少一个完整的电价峰谷变化,同时不至于让远端预测误差严重污染优化。光伏预测和负荷预测在4小时内的平均绝对百分比误差一般能控制在10%以内,MPC的鲁棒性可以消化这部分误差。
4.2 权重系数的调节经验
目标函数里的权重系数,每一组都代表一种"经营策略":
| 权重 | 初值建议 | 调节方向 | 物理含义 |
|---|---|---|---|
| 购电成本权重 (w_{price}) | 1.0 | 升高则更激进地套利 | 电价敏感度 |
| SOC参考权重 (w_{soc}) | 0.5 | 升高则SOC更稳 | 电池寿命偏好 |
| 并网功率平滑权重 (w_{grid}) | 0.2 | 升高则功率波动更小 | 配电网友好度 |
| 光伏弃光惩罚权重 (w_{curtail}) | 100.0 | 尽量大,避免无谓弃光 | 新能源利用率 |
第一次跑通代码时,我是把购电成本权重设成1.0,其他权重全部0.1。结果发现并网功率曲线震荡得很厉害,因为MPC只盯着电价走,在电价临界点附近不断切换电池充放电状态。后来我把平滑权重调高到0.3,震荡显著改善。调参经验是:每次只改变一个权重,观察三类输出(并网曲线、SOC曲线、总电费),别一次性动多个参数。
4.3 求解器的选择:OSQP还是IPOPT
cvxpy默认求解器是OSQP(Operator Splitting Quadratic Program),这是ADMM算法族的求解器,适合处理大规模二次规划问题。我测试过它和IPOPT的区别:OSQP对凸二次问题的求解速度确实快,但这个项目里有个微妙点——当目标函数里出现cp.maximum这种非光滑函数时,问题从严格的QP变成了凸分段线性问题,OSQP的表现就比较一般了。
我的解决办法是:把SOC越限惩罚改成平方形式,用cp.maximum(0, soc[k]-soc_max)**2替换原来的线性最大项,这样整个问题仍然是凸QP,OSQP的ADMM收敛性稳定得多。如果你有比较多的一阶逻辑约束(比如"充电和放电不能同开"这种强互斥),不如直接换成ECOS或SCS求解器,或者转成混合整数二次规划(MIQP)用Gurobi之类商业求解器。
5. 仿真结果与调度策略分析
5.1 典型日的调度结果解读
我拿一个园区微电网的典型日数据做了仿真,数据包含15分钟分辨率的光伏实测、负荷实测和峰谷电价。前几天用MPC开环策略和传统基于规则策略做了对比。基于规则策略是固定的:夜间谷电充电、日间光伏充裕时电池放电、晚高峰电池削峰。MPC则展现出两个明显优势。
第一个优势在早上6:00到10:00这个区间。规则策略死板地执行"谷电充电到80%",结果上午光伏输出猛增,电池都已经充得很足,只能被迫弃光。而MPC看到未来4小时的光伏预测后,主动减少了早上的充电量,SOC在午前留出冗余空间,光伏出力几乎全部被消纳,只有个别时段轻微弃光。全天光伏弃光率从9.2%降到1.8%,这个改善幅度对现场运行来说已经非常可观。
第二个优势体现在晚高峰的电网功率:MPC在16:00提前把电池充满,然后在17:00到20:00电价峰值区间集中放电,并网购电功率被压得比规则策略低三成。全天电费开支比规则策略节省了12.6%。用MPC不是简单地"低价充、高价放",而是综合考虑了光伏预测的不确定性、SOC的安全边际和发电机启停成本,给出的决策序列更协调。
5.2 储能SOC变化曲线与寿命影响
从仿真SOC曲线可以看到,MPC策略下SOC的日变化范围基本控制在42%到88%之间。每次充放电循环的深度没有突破90%,也没有跌破40%——这对延长锂电池循环寿命很有意义。
寿命管理的另一层考量是充放电次数。规则策略在电价波动频繁时容易出现来回切换,一天内电池动作次数多,等效循环次数高。MPC策略因为目标函数里有并网功率平滑项和SOC参考惩罚项,天然会抑制频繁反转。我可以看动作频次指标:规则策略一天内充放电方向切换14次,MPC只有6次。这意味着MPC策略下的储能系统等效日历寿命更长。
5.3 发电机参与调度时的燃油经济性表现
如果微电网里有一台柴油发电机,MPC还能发挥调度排序的优势。仿真场景设定成连阴天,光伏出力低,储能容量受限,必须靠发电机补足缺口。我的目标函数里,发电机的成本函数写成了二次多项式:(C_{gen} = 0.15P_{gen}^2 + 0.05P_{gen} + 启动惩罚)。MPC会自动把发电机安排在接近经济运行点的出力水平,而不是简单地对负荷波动做跟随。
对比仿真显示:MPC调度下的发电机全天油耗比规则策略低8.3%。规则策略在负荷骤升时让发电机急冲满负荷,MPC则会让储能先扛负荷,发电机平缓爬坡——这个优先次序其实是目标函数里各项权重的自然涌现。这就是MPC落到微电网调度上比较吸引人的地方:复杂优先逻辑不需要你写死,只要把成本函数和约束设对,最优行为自己长出来。
6. 实际运行中踩过的坑与排查技巧
6.1 数据预处理是MPC的成败线
MPC性能的上限其实不是被算法决定的,而是被数据质量决定的。我踩过最大的坑是光伏数据的异常值:有一次某天的光伏序列里出现一个500kW的尖峰,而当天实际出力只有200kW。MPC看到预测里有500kW,就大幅削减了电池的充电功率,结果实际光伏不够用,并网点功率直接冲到上界以外,系统功率不平衡。
排查思路是:在数据进入MPC之前,加一个基于中位数滑动窗口的异常值清洗模块。我用的是简单可靠的Hampel滤波器,窗口长度11,异常阈值3倍中位数绝对偏差(MAD)。处理后第90百分位预测误差从19%降到11%。
另一个容易被忽视的问题是数据对齐。光伏和负荷数据来自不同的SCADA点位,时间戳可能差半分钟。十五分钟一个调度周期,这半分钟误差在功率平衡上是不可接受的。我在data_loader里统一重采样到15分钟整数刻度,并在MPC里加了当前时刻的实时功率反馈项——这个反馈项直接修正状态模型里的 (SOC(k)) 初值,而不是完全信任递推结果。
6.2 无可行解的根因分析法
MPC求解返回infeasible是大家都会遇到的问题。排查次序我建议固定下来,效率会高很多:
- 第1步:检查SOC初始值是否越界。如果当前SOC是0.92,而上下限是0.9,直接无解。稳妥做法是求解前把SOC数值夹在上下限内,并配合软约束——把SOC上下限改成带松弛变量的软约束。
- 第2步:检查功率平衡约束是否有物理矛盾。比如光伏预测为0,负荷预测为150kW,储能处于禁充禁放状态,发电机容量又是80kW,此时功率缺额无解。实际解决:给功率平衡加一个松弛变量(相当于虚拟负载),权重大一点,先保证问题可解,再观察松弛变量去定位矛盾时段。
- 第3步:检查预测数据是否有NaN或负值。有个隐藏问题——负荷曲线偶发为零点几的负值,可能是仪表校准漂移导致。如果负荷预测出现负值,功率平衡约束就会要求"光伏+电网+储能"倒灌功率给负荷,形成不合理解。数据清洗环节加一条硬规则:负荷最小值为零。
6.3 数值病态矩阵的预防
MPC里的状态递推矩阵展开后,如果预测时域很长(比如40步以上),B矩阵的条件数会较大,容易出现数值不稳定。我在项目里加入了对角缩放(Diagonal Scaling):把决策变量的物理量纲统一到同一数量级,比如功率变量用kW的数级,SOC用百分比数值除以100。这样能有效降低条件数,让OSQP在默认容差下表现得比较稳定。
如果你用的是casadi的IPOPT,处理数值病态的风格是加大tol(从1e-8放宽到1e-6),同时把print_level调成5,观察每一步的KKT条件残差变化,定位病态来源。IPOPT对初始点也比较敏感,我在项目里把上一周期的最优解作为这周期的热启动初值,求解速度大约提升40%。
6.4 常见问题速查表
| 现象 | 根因 | 处理方案 |
|---|---|---|
| SOC曲线剧烈震荡 | SOC参考项权重过低,或预测数据噪声大 | 增大 (w_{soc}),数据清洗加强 |
| 充放电频繁切换 | 均衡价格附近来回跳动 | 加功率爬坡约束,提高并网功率平滑权重 |
| 电池总充不满 | SOC_ref设置太低 | 将SOC_ref设为0.7,弃光惩罚权重调大 |
| 仿真后期出现无解 | 预测数据截断补值有误 | 检查padding方式,改用镜像边界 |
| 购电功率越限 | 功率平衡约束漏了<= p_grid_max | 检查约束列表完整度 |
| 求解耗时过长 | 预测时域过长或变量过多 | 缩短Np,用热启动,或者换稀疏求解器 |
7. 延伸扩展:从离线仿真到在线部署
7.1 硬件在环测试的过渡方案
很多人把MPC调好后直接往PLC里灌代码,这是非常危险的做法。建议先在软件在环(SIL)环境做闭环测试,用一个虚拟微电网模型接MPC控制器,连续跑一周甚至一个月的数据。我在离线仿真跑稳定后,又用OPAL-RT做了半实物测试,把MPC算法封装成独立函数,通过UDP报文和虚拟实时仿真接口交互,每15分钟一次循环。这个过程中发现,实际通信延迟会引入2~3秒的决策滞后,所以MPC里我加了一个时间同步模块,对预测窗口的起始时间做了补偿——把预测窗口起点设为"当前时间+通信延迟"。
7.2 与深度学习预测结合的升级方向
MPC输入质量直接取决于预测模块。我目前的预测还是基于历史数据的加权移动平均和简单时序分解,如果你想让系统精度更高,完全可以给MPC接一个深度学习短期预测模型,用LSTM或Transformer预测光伏和负荷。这些模型的输出概率区间比点预测更适合MPC,因为MPC可以设计成随机模型预测控制(Stochastic MPC),让约束以概率形式成立,而不是死板的硬约束。
7.3 代码工程化的几个细节
从单机脚本变成可持续维护的项目,有几个走过弯路后形成的习惯值得分享:
- 配置和模型分离:把预测时域、权重系数、电池容量全部抽到config.yaml里,不要硬编码在Python文件中。换场地改参数时,不用碰核心算法代码。
- 日志记录完整化:每轮MPC求解完,把目标函数值、求解器状态、是否违约、求解耗时写入日志。线上跑一个月后这些记录是定位问题的关键线索。
- 结果存档带版本号:改动代码前,旧模型跑出的结果必须存档,最好加个模型版本号。有一次我调整了SOC参考值,再对比改进效果时差点找不到原版本的数据,白白做了几轮无效对比。
我在实际跑这个MPC调度系统的过程中,最大的体会是:MPC落地的主要挑战从来不是算法本身,而是和数据、执行机构、系统约束之间的衔接。只要预测数据靠谱、约束表达贴近物理、权重参数反映运营目标,MPC在微电网调度里带来的经济收益和运行平稳性提升是很可观的。如果你正要上手这个方向,建议先把本文的仿真闭环跑通,再逐步替换成你实际场景的数据和约束,这个路径最不容易绕弯路。