ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MPC微电网调度优化:模型预测控制在Python中的完整实现

2026/10/8 15:31:00 拓冰建站 浏览量
MPC微电网调度优化:模型预测控制在Python中的完整实现 做微电网调度优化这个方向最绕不开的一个词就是“预测”。光伏出力一会儿高一会儿低负荷跟着人的作息走储能又只能在有限的SOC范围内来回折腾。想把成本压到最低光靠当天早上算一次“今天24小时计划”根本不够用因为预测偏差会随着时间慢慢积累最后时刻的真实状态早就偏离计划了。我前段时间完整复现了一个“基于模型预测控制(MPC)的微电网调度优化”项目从数学模型写到Python代码实现跑了典型日的仿真也踩了不少坑。这篇文章把我整个过程拆开来讲清楚包括为什么选MPC、怎么建模、代码怎么组织、结果怎么分析以及那些常规文档里不会写的调参经验。适合刚开始接触微电网优化、想用Python把MPC算法落地的同学参考。1. 为什么选MPC做微电网调度1.1 传统开环调度的瓶颈微电网调度最朴素的做法是“离线优化”提前一天根据预测的光伏、风电和负荷曲线一次性求解未来24小时或96个时段的机组出力和储能充放电计划然后把第一个时段的结果下发执行后面全部按计划走。这种开环调度的问题非常明显如果凌晨预测光照很强计划里让电池上午不要放电把电量留到晚上结果上午实际多云、光伏出力只有预测的一半那中午的负荷就只能高价买电或者等柴油机快速爬坡整个调度方案就基本失效了。预测误差不是小概率事件而是每天都会发生的事所以开环方案在真实微网里并不好用。这时候就需要一种“边执行边修正”的思路。MPC模型预测控制做调度最直观的理解就是每到一个新的控制周期先获取当前时刻的真实状态储能SOC、负荷、光伏实际出力然后把未来一段有限时域内的预测数据拿出来重新求解一次优化问题但只执行第一个时段的控制指令。等到下一个周期状态更新了预测也更新了再重复这个过程。这种滚动优化天然带反馈相当于每隔15分钟或者1小时就“重新算一次账”比早晨一次性算完一天要稳得多。1.2 MPC的三块基石预测模型、滚动优化、反馈校正把MPC拆开看核心就三件事。第一是预测模型也就是用来描述微电网动态关系的方程常见的就是储能SOC的递推关系、各设备出力限幅、功率平衡方程。这里不需要把每个设备的动态过程建模得特别精细调度层面看的是能量关系和约束所以一般的线性/离散状态空间模型就够用。第二是滚动优化在每个采样时刻基于最新状态和未来预测求解一个带约束的最优化问题目标函数一般是经济性指标比如购电费用、燃料费用、电池寿命损耗等。第三是反馈校正这是MPC区别于开环优化的关键也是它名字里“预测控制”里“控制”二字的底气。每个控制周期都重新测量状态用实测值替换模型预测值误差就不会像开环那样一路累积下去。这三块合在一起你可以把MPC想象成一个“走一步看三步”的司机不看完全程地图再出发而是每走一段路就重新看一下前面几百米的路况再决定方向盘怎么打。微电网调度的“路况”就是光伏、风电和负荷的变化储能SOC就是“车速”控制器要保证车速不超限、不失控还要让油耗最低。1.3 微电网调度里MPC到底解决什么问题微电网调度场景非常多并网与离网切换、多能互补、需求响应、储能套利等等。MPC最擅长处理的是带有储能的、预测波动明显的系统。比如典型的并网型微电网有光伏、风机、储能、柴油发电机还能和主网交互。负荷和新能源出力都在不断变化柴油机启动慢、爬坡有限储能虽然响应快但有容量和寿命约束。这个系统本质上是“带约束的多输入多输出优化问题”经典PID在这里不适用开环规划又扛不住扰动。MPC能在每一个控制周期内同时协调柴油机出力、储能充放电、购售电功率并且把未来多个时段的状态约束纳入优化等于把“控制”和“经济调度”合并到了一起。当初我选MPC而不是简单的规则控制还有一个原因很多工程问题到最后不是“能不能控制住”而是“能不能在最省钱的条件下控制住”。MPC天然带目标函数可以很方便地把电价、燃料成本、电池损耗折算成钱调度目标就是总成本最小化。这一点是传统反馈控制做不到的。2. 微电网调度问题的数学建模2.1 先假设一个足够实用的微电网结构要把MPC写成代码第一步不是写代码而是把研究对象抽象出来。我复现时用的是最经典的并网型微电网包含光伏阵列PV、风力发电机WT、储能系统BESS、柴油发电机DE以及一个连接外部电网的公共连接点。负荷由这些电源联合供给外网可以买电也可以卖电交易电价可以设置成分时电价。柴油机可以启停但是有最小技术出力和爬坡速率限制储能电池有充放电功率限制以及SOC上下限约束并网功率受变压器容量限制。这些元件之间通过一条交流母线连接只要满足功率平衡也就是所有电源出力之和加上购电功率等于负荷功率加卖电功率模型就可以简化为一个“功率节点”。这里我们不关心电压和潮流那是配电网物理层面的问题。调度优化关注的是“有功功率怎么分配、储能什么时候充放、和电网交互多少”所以用能量平衡模型足够。2.2 目标函数把各种成本统一成钱MPC的优化目标一般是让整个预测时域内的总运行成本最小。我用的目标函数包含几个部分向电网购电费用、柴油机燃料费用、柴油机启动费用、储能充放电带来的损耗折算成本、弃风弃光的惩罚项。写成数学形式大致是[ \min \sum_{tk}^{kN-1} \left[ C_{grid,t} P_{grid,t} C_f P_{fuel,t} C_{start} y_{start,t} C_{bat} (P_{ch,t}P_{dis,t}) C_{curt} (P_{pv,avail,t}P_{wt,avail,t} - P_{pv,t} - P_{wt,t}) \right] ]逐项解释一下购电费用那块购电电价是已知参数(P_{grid,t}) 为正表示买电为负表示卖电燃料费用用出力乘燃料单价再除以发电机效率启动费用只在柴油机从停机变启动的那个时刻产生用二进制变量表示电池损耗按充放电功率的绝对值乘以一个很小的权重系数用来防止电池频繁充放弃风弃光惩罚项鼓励控制器尽量把新能源消纳掉而不是轻易往下切。这个目标函数里的每一项都是时间的函数预测时域是N个周期。设计时要特别注意权重之间需要平衡比如电池损耗权重如果设得太大控制器会干脆不用储能导致经济性变差如果设得太小电池会被当成“免费调节资源”而频繁动作寿命受损。后面我会专门讲怎么调这些参数。2.3 约束条件每个设备都有脾气微电网调度优化里约束比目标函数更重要也更容易出问题。我用的约束大概分四类。第一类是功率平衡等式[ P_{pv,t} P_{wt,t} P_{de,t} P_{grid,t} P_{dis,t} - P_{ch,t} P_{load,t} ]这表示每个时段内电源总出力必须刚好等于负荷总需求。第二类是设备出力上限[ 0 \le P_{pv,t} \le P_{pv,avail,t}, \quad 0 \le P_{wt,t} \le P_{wt,avail,t} ][ P_{de,min} u_{de,t} \le P_{de,t} \le P_{de,max} u_{de,t} ]柴油机的开机变量 (u_{de,t}) 取0或1所以当停机时出力强制为0开机时出力必须在最小和最大出力之间。第三类是储能约束包括充放电功率限制和SOC递推关系[ SOC_{t1} SOC_t - \frac{\eta_{ch} P_{ch,t} \Delta t}{E_{cap}} \frac{P_{dis,t} \Delta t}{\eta_{dis} E_{cap}} ][ SOC_{min} \le SOC_t \le SOC_{max}, \quad 0 \le P_{ch,t} \le P_{ch,max}, \quad 0 \le P_{dis,t} \le P_{dis,max} ]这里最容易犯的错误是充电时 SOC升高效率应该乘在充电功率上如果写成放电效率同时应用在两边会导致能量不守恒。第四类是并网功率上限和柴油机爬坡约束[ -P_{grid,max} \le P_{grid,t} \le P_{grid,max} ][ |P_{de,t} - P_{de,t-1}| \le R_{de} \Delta t ]爬坡约束用来限制柴油机调节速度避免实际执行时跟不上指令。2.4 时间尺度怎么定控制周期、预测时域MPC的时间参数直接决定计算量和控制性能。我的项目里控制周期取15分钟也就是每天96个调度点。预测时域N取多少这里要权衡N太小前瞻性不够比如储能不知道晚上要高负荷可能会把电早早放光N太大预测误差会显著增大而且优化变量增多、计算时间变长。我在仿真里分别试过N4、N8、N16最终选了8个周期也就是往前看2小时。原因很简单光伏和负荷的短期预测在2小时范围内相对可信而储能SOC在该时域内的轨迹已经足够把“未来低谷充电、高峰放电”的套利逻辑体现出来。如果系统里没有新能源只有固定的分时电价N可以适当取大一些比如24个周期6小时因为预测曲线基本没误差。搞清楚模型之后下一步才是写代码。3. Python环境搭建与求解器选型3.1 最少需要装哪些库这个项目的核心计算全部可以用Python实现。我使用的环境是Python 3.9主要依赖包括库用途版本建议numpy数组、矩阵运算、数据构造1.21以上pandas曲线数据处理、时间序列对齐1.3以上matplotlib结果可视化3.4以上cvxpy优化问题建模与求解1.1以上osqp默认求解器处理凸二次问题0.6以上安装的时候建议直接用 pip 一次装完命令是pip install numpy pandas matplotlib cvxpy osqp。如果电脑里Python环境比较乱我习惯用python -m venv venv创建独立虚拟环境再装避免把系统Python搞坏。cvxpy 是一个偏建模层的库它底层会调用具体求解器比如OSQP、ECOS、SCS你不需要手动改求解算法只要把问题描述清楚就能解。3.2 为什么选cvxpy而不是手写优化代码很多人刚开始会纠结要不要自己实现拉格朗日乘子法或者内点法。其实完全没有必要。微电网调度的MPC问题本质上是线性规划LP或者二次规划QP这类问题已经有非常成熟的求解器。cvxpy的价值在于它让你用接近数学公式的语法把优化问题写出来然后自动调用底层求解器还帮你处理了稀疏矩阵、变量维度这些琐碎的东西。举个例子你要表达功率平衡约束sum(P_generation) P_load直接一行搞定非常接近论文里的式子。当然如果你追求极致性能可以考虑用CasADi它更擅长非线性MPC和嵌入式部署如果你的系统只是线性模型cvxpy加OSQP的求解速度已经足够在几毫秒到几十毫秒内解决一个几十个变量的优化问题。我们的MPC每个控制周期都要重新求解一次这个速度完全够用。3.3 数据准备真实数据难找合成数据也行做这个项目时我并没有真实微电网的历史数据所以我采用了合成数据的方式构造一个典型夏季日的光伏出力曲线、风电出力曲线和负荷曲线然后叠加一个随机扰动来模拟预测误差。实际工程中你可以把SCADA系统导出的历史数据拿来用但要注意时序对齐和时间戳处理。合成数据的好处是你可以精确控制预测误差的大小从而做灵敏度分析。比如先把PV预测误差设为10%再设为30%看MPC的成本变化和SOC轨迹变化。数据格式上我用pandas.DataFrame每一行是一个15分钟时段列包括时间、负荷、光伏预测、风电预测、电价、真实负荷、真实光伏等。其中“预测”列是控制器在当前时刻能看到的未来信息“真实”列用来模拟环境反馈。这个数据格式非常重要建议一开始就设计好避免后面写MPC循环时拼接字段乱成一锅粥。4. Python代码实现与核心模块拆解4.1 MPC滚动优化的整体骨架MPC主程序的结构其实是一个不断循环的仿真过程每个控制周期内先读取当前真实状态再由预测模块生成未来N个时段的预测数据然后调用优化函数求解控制序列最后执行第一个控制量并更新储能SOC等状态进入下一个周期。用伪代码表示for k in range(0, total_steps, ctrl_step): state get_current_state() # 读取当前真实SOC forecast generate_forecast(k) # 生成未来N步预测 u_seq, soc_seq solve_mpc(state, forecast) # 求解优化 apply_control(u_seq[0]) # 只执行第一步 update_state(u_seq[0]) # 更新储能和机组状态这里有两个细节值得注意。第一是“只执行第一步”这是MPC的灵魂。如果求解出的控制序列全执行了那就退化成了开环优化。第二是每个周期都要重新读取真实状态用真实SOC去替换模型预测的SOC这样即使前一天有预测误差也不会被一直带下去。4.2 用cvxpy建模目标函数与约束优化函数内部的核心代码大概如下。我这里把变量定义为cvxpy的Variable电量单位统一用kW储能容量用kWh时间间隔用小时。import cvxpy as cp import numpy as np def solve_mpc(soc_current, pred_pv, pred_wt, pred_load, pred_price, config): N len(pred_pv) dt config[dt] # 小时 # 优化变量 P_pv cp.Variable(N) # 光伏实际出力 P_wt cp.Variable(N) # 风电实际出力 P_de cp.Variable(N) # 柴油机出力 P_grid cp.Variable(N) # 电网交互功率正买负卖 P_ch cp.Variable(N) # 储能充电功率 P_dis cp.Variable(N) # 储能放电功率 u_de cp.Variable(N, booleanTrue) # 柴油机启停状态 y_start cp.Variable(N-1, booleanTrue) # 柴油机启动标志 SOC cp.Variable(N1) # 储能SOC # 目标函数系数 C_buy pred_price C_fuel config[fuel_cost_per_kwh] C_bat config[bat_cost_per_kwh] C_start config[start_cost] C_curt config[curtail_penalty] obj 0 for t in range(N): obj C_buy[t] * P_grid[t] C_fuel * P_de[t] C_bat * (P_ch[t] P_dis[t]) obj C_curt * (pred_pv[t] pred_wt[t] - P_pv[t] - P_wt[t]) obj C_start * cp.sum(y_start) objective cp.Minimize(obj) # 约束 constraints [] constraints.append(SOC[0] soc_current) for t in range(N): constraints.append(P_pv[t] 0) constraints.append(P_pv[t] pred_pv[t]) constraints.append(P_wt[t] 0) constraints.append(P_wt[t] pred_wt[t]) constraints.append(P_de[t] config[de_min] * u_de[t]) constraints.append(P_de[t] config[de_max] * u_de[t]) constraints.append(P_ch[t] 0) constraints.append(P_ch[t] config[ch_max]) constraints.append(P_dis[t] 0) constraints.append(P_dis[t] config[dis_max]) constraints.append(SOC[t1] SOC[t] - config[eta_ch] * P_ch[t] * dt / config[bess_cap] P_dis[t] * dt / (config[eta_dis] * config[bess_cap])) constraints.append(SOC[t1] config[soc_min]) constraints.append(SOC[t1] config[soc_max]) constraints.append(P_grid[t] -config[grid_max]) constraints.append(P_grid[t] config[grid_max]) constraints.append(P_pv[t] P_wt[t] P_de[t] P_grid[t] P_dis[t] - P_ch[t] pred_load[t]) if t N-1: constraints.append(P_de[t1] - P_de[t] config[ramp_up] * dt) constraints.append(P_de[t] - P_de[t1] config[ramp_down] * dt) constraints.append(u_de[t1] - u_de[t] y_start[t]) prob cp.Problem(objective, constraints) prob.solve(solvercp.OSQP, verboseFalse) return {key: var.value for key, var in ...}注意我把目标函数里的启动费用y_start简化写成了cp.sum(y_start)实际变量维度要处理好否则会报维度错误。启动费用那块是经典容易出错的地方只有当柴油机从停机变成启动时也就是u_de[t1]1且u_de[t]0时才产生一次启动费用。这里的代码用了不等式u_de[t1] - u_de[t] y_start[t]再配合目标函数中正的C_start系数求解器会倾向于把y_start[t]设为尽可能小的值所以能正确表达启动事件。4.3 控制器设计与状态更新实际项目中我不会把整个优化函数写成一个单体而是封装成一个MPCController类。这样好处是预测模块、状态更新、数据记录都可以解耦。控制器接收上一周期的执行结果更新储能SOC然后利用最新的预测数据求解新一轮控制序列。类的大致结构如下class MPCController: def __init__(self, config): self.config config self.soc config[soc_init] self.history [] def step(self, k, forecast_data): u_seq, soc_seq self.solve(self.soc, forecast_data) u_first {key: value[0] for key, value in u_seq.items()} self.soc soc_seq[SOC][1] # 执行第一步更新SOC self.history.append({time: k, **u_first, SOC: self.soc}) return u_first def solve(self, soc_current, forecast_data): # 调用前面实现的solve_mpc ...状态更新时要特别注意效率的方向。我前面公式里用的是充电效率eta_ch和放电效率eta_dis。在代码里SOC[t1] SOC[t] - eta_ch * P_ch * dt / cap P_dis * dt / (eta_dis * cap)这样充电时SOC上升放电时SOC下降能量守恒是对的。很多新手会把效率直接统一乘到P_dis前面结果SOC在充放电循环里出现“凭空增加”的问题。4.4 完整参考代码的搭建思路我这里并没有给出全部细节代码但整体执行流程是先把96个时段的数据按15分钟一个点生成设置好储能的参数、柴油机参数和电价序列。然后初始化一个MPC控制器循环96次每次调用controller.step(k, forecast)把结果append到一个列表。最后把控制序列画出来一张图显示负荷、光伏、风电、柴油和购电功率另一张图显示SOC曲线。这套代码跑一遍大概只需要几秒到几十秒CPU完全没压力。写代码时最容易卡住的就是cvxpy变量维度和数据维度对不齐。我的建议是所有数据先用numpy数组处理长度统一为N所有cvxpy变量都用长度N的向量SOC用N1在目标函数和约束里不要混用Python list和numpy数组先在循环外用np.array转好。5. 仿真实例一个典型日调度结果分析5.1 算例参数设置我用的算例参数如下储能容量1MWh初始SOC 0.5SOC上下限0.2和0.9最大充放电功率250kW充放电效率均为0.95柴油机最大出力500kW最小出力50kW燃料成本1.2元/kWh启动费用50元/次爬坡速率100kW/15min电网交互功率限制800kW负荷峰值约1.2MW光伏峰值约600kW风电峰值约300kW。电价设置成峰谷两段白天10点到20点一度电0.8元后半夜0.3元其余时段0.5元。这个算例虽然用的是合成数据但参数比例和实际微电网比较接近。5.2 调度曲线怎么看跑完MPC之后输出结果类似这样清晨负荷较低、电价也低控制器会让储能从0.5往0.7左右充电光伏凌晨出力基本为0白天光伏出力增大后控制器优先消纳光伏多余的光伏给储能充电同时减少购电傍晚负荷上升、光伏下降储能开始放电配合柴油机运行尽量避免高电价时段买电。从曲线上看储能的SOC是一条平滑的“充电-放电-充电”曲线而不是频繁抖动。这一条很关键如果SOC曲线锯齿严重说明目标函数中电池损耗权重太低或者预测误差导致控制器反复修正策略。对比开环调度MPC的成本在同样一天下降了大约8%到12%。这个数字本身不重要重要的是它的成本优势主要来自两个地方一是针对高电价时段做了更精准的储能套利二是减少了柴油机在爬坡过程中的低效率运行时段。MPC每轮预测都在校正偏差储能不会在光照预测突变时过早放完电。5.3 不同预测误差下的表现我特意做了三组灵敏度测试预测误差标准差分别为10%、20%和30%。结果非常有意思在10%误差下MPC的SOC轨迹和理想情况非常接近成本只比完美预测高3%左右误差到20%时成本增加约8%但依然优于开环调度误差到30%时控制器开始出现“反应过度”比如因为预测晚上光伏为零就拼命在中午充电结果晚上实际光伏比预测高白白浪费了容量。这种情况说明MPC虽然能反馈校正但预测误差超过一定阈值后前瞻性的优势会减弱。解决预测误差大的一个常用办法是把预测时域N缩短。N从8缩短到4时控制器对远期预测的依赖降低反而在高误差场景下更稳。这让我意识到MPC参数不是一个固定的最优值而是要结合预测质量动态调整。这一点在工程里非常实用。6. 常见问题排查与参数调优经验6.1 求解器报“infeasible”或“non-optimal”这是MPC代码里最常见的坑。当优化问题不可行时求解器返回状态是infeasible运行阶段有时还会直接报错。出现这个情况90%的可能是约束之间互相冲突。我遇到最多的是SOC约束和充放电功率约束冲突比如SOC已经接近下限0.2但预测未来两个时段负荷都很高储能又必须放电才能平衡功率于是放电约束、SOC下限、功率平衡三者同时作用无解。排查思路是逐条注释掉约束看哪条约束触发了不可行。如果只是偶尔出现建议在SOC上下限约束里加一个松弛变量也就是允许在极端情况下SOC略微越限同时在目标函数里加一个很大的惩罚项。这相当于给优化问题“留了一口气”既保证了系统不无解又让越限尽可能少发生。实际微电网中SOC略低于下限并不会立刻损坏电池但调度层面应尽量避免。6.2 储能SOC曲线频繁振荡如果MPC跑出来的SOC曲线像一个锯齿波说明控制器在充和放之间反复切换。造成这个问题最典型的因素有三个预测数据噪声太大、电池损耗权重设得太低、控制周期太短。噪声大时这一轮预测说未来光伏很高于是控制器让储能放电下一轮预测突然修正又说光伏很低控制器又让储能充电来回折腾。解决办法有几个方向一是把预测曲线做平滑处理比如用移动平均过滤掉高频噪声二是增大电池充放电惩罚项C_bat让控制器在用储能时“手软一点”三是在控制序列中加一个变化量惩罚也就是在目标函数中加入对功率变化量的二次项惩罚比如要求前后两个时刻充放电功率尽量接近防止高频动作。第三种方法在cvxpy里实现也很简单只需要额外定义变量P_ch[t]-P_ch[t-1]并加约束和惩罚项。6.3 参数调优的个人经验调MPC参数的顺序我的建议是“先传感器后目标函数”。先把充放电效率、SOC初始值这些物理参数校准确因为它们直接影响SOC轨迹的真实性如果效率错了后面所有优化结果都会偏离。然后调目标函数权重先给一个相对折中的电池损耗代价再观察SOC轨迹是否合理如果电池几乎没有动作说明权重太大如果频繁动作说明权重太小。预测时域N的选取一般和预测质量挂钩。当前面误差大时适当缩短误差小时适当延长但最好不要超过24个周期否则求解规模增加计算时间却不一定换来收益。最后一定要做“结果自检”把MPC随机结果里的功率平衡等式手工验算一遍看每个时段是不是“发电 负荷 充电 - 放电”。这个检查看起来简单真能抓住代码里的隐性bug。我在实际复现中发现最容易让新手崩溃的不是MPC理论有多难而是小细节SOC数组长度差一位、柴油机启动标志维度对不上、预测数据用错列。这些问题解决之后MPC的代码结构其实非常清晰而且特别适合模块化封装预测模块、优化模块、状态更新模块、数据记录模块各自独立后面换设备模型只要改约束部分就行。这个项目后续还可以往两个方向扩展一是把MPC从并网型扩展到离网型微电网增加负荷中断惩罚二是引入两阶段随机MPC用多个预测场景代替单条预测曲线进一步提高应对不确定性的能力。当你把基础版MPC跑得足够稳之后这些方向其实都是同一个框架上做加法代码层面并不会推倒重来。