ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python实现MPC微电网调度优化:滚动优化与储能能量管理实战解析

Python实现MPC微电网调度优化:滚动优化与储能能量管理实战解析 做微电网调度优化这两年我接触最多的就是MPC模型预测控制这类滚动优化方法。这套用Python实现的项目不只是一个算法脚本而是把预测、优化、反馈闭环地串起来解决实际日内的能量管理问题。光伏出力在变、负荷在变、分时电价也在变微电网怎么在满足用电需求的前提下尽可能省钱MPC是一个很适合的答案。如果你正在做微电网调度优化、或者想把手上的MPC算法从Matlab迁移到Python这篇内容可以直接参考。我在这套代码里实现了完整的MPC微电网调度框架光伏和负荷预测数据输入、储能电池约束建模、分时电价场景下的滚动优化、与日前调度DA的对比分析。这篇文章会把整体思路、数学模型、核心代码、调参经验和踩坑记录都整理出来尤其是那些预测时域、终端约束、SOC离散化等容易出问题的地方我会讲得细一些。1. 为什么用MPC做微电网调度从整体思路说起1.1 微电网调度在解决什么问题微电网本质上就是一个中小规模的电力系统常见的组成是光伏、储能电池、本地负荷还可以和外部大电网并网。调度问题说白了就是在未来的每个时段决定电池是充电、放电还是闲着决定从电网买多少电使得一天的运行成本最低同时不能突破设备的物理限制。听起来像是一个经典的优化问题确实如果全天光伏出力和负荷曲线完全已知这个优化可以直接一次性求解这就是日前调度Day-Ahead的思路。但实际运行中预测永远有偏差。光伏预报说中午能发120kW结果云飘过来只剩60kW用户负荷也不可能分毫不差地按预测走。一旦实际和预测对不上开环的日前调度方案就会失效可能电池该充电时没充够该放电时又提前放完了。MPC应对这种不确定性的方式很直接不一次算到底而是“走一步看一步”。每个控制周期只做未来一段时间的优化算完只执行第一个时段的指令到了下一个周期再用最新的实测状态重新计算。这样每次决策都是基于当前真实状态预测偏差带来的影响就被不断修正。这也解释了为什么MPC在微电网调度里越来越受欢迎。它本质上是一种带反馈的在线优化方法既有优化能力又有应对不确定性的弹性。和传统PID那种“看到误差再纠正”的方式不同MPC可以提前看到未来几个小时的预测信息主动做出预判这在电价有峰谷、光伏有昼夜节律的场景下尤其有价值。1.2 MPC相比传统开环优化的三个优势先说说我理解的MPC为什么比开环优化更实用这直接决定了代码怎么设计。第一是滚动优化。MPC不会一次性把96个时段全定死而是只关心当前窗口内的决策。窗口推进决策也跟随新的信息更新不会一条路走到黑。这就像一个司机开车虽然出发前看了导航规划了全程但路上还是时刻根据实时路况调整路线。第二是反馈校正。每个控制周期开始时我都用当前实际的SOC作为下一轮优化的初始状态而不是用模型推导出的SOC。别小看这个动作电池SOC的估计误差、充放电效率的偏差、未知的扰动都会在滚动过程中被实际状态“拉回来”。这是MPC能在预测不准时依然保持可用的关键。第三是约束处理能力强。电池SOC不能超限、充放电功率有上限、和电网的交换功率有下限这些物理约束在MPC框架里可以直接作为约束条件交给求解器处理不需要像传统控制那样做复杂的约束转化。实际项目中边界条件不可违反是刚需MPC这一点很省心。不过MPC也不是没有代价。它比日前调度多了一个“预测—求解—执行—再预测”的循环对预测数据的依赖更强计算频率也更高。如果光伏和负荷预测做得太差MPC的优势会大打折扣。所以做这个项目时我把预测数据和扰动模拟也一并做了方便测试MPC在恶劣预测条件下的表现。2. 系统建模与参数设置调度问题的“前端工程”建模是整个项目的地基。模型没建好后面代码写得再花哨也白搭。这一节我把场景假设、参数设计、目标函数和约束条件都拆开讲清楚。2.1 场景假设与基础数据我参考了常见的微电网调度比赛题目和实际工程项目搭建了这样一个基本场景一个并网运行的工业园区微电网包含光伏发电系统、储能电池系统和本地负荷并且与外电网可以双向交换功率。为了聚焦MPC本身我做了两个简化一是只考虑有功功率忽略无功和电压问题二是假设光伏和负荷的预测数据已经通过其他方法得到本项目重点放在调度决策层。基础参数我按一个中等规模的园区来设调度周期1天采样间隔15分钟共96个时段光伏装机容量200kW按典型日辐射曲线生成出力负荷峰值250kW谷值60kW带有一定波动电池容量300kWh最大充放电功率50kWSOC范围0.1~0.9电池充放电效率均取0.95初始SOC0.5仿真结束时不强制要求SOC回落到初始值并网购电无功率上限但不允许向电网售电实际项目中如果想允许售电改一下变量的边界就行分时电价是调度决策的核心驱动因素我按常见的峰平谷三段设置谷段23:00-07:000.3元/kWh平段07:00-10:00、15:00-18:00、21:00-23:000.6元/kWh峰段10:00-15:00、18:00-21:001.0元/kWh这个电价结构很有代表性。谷段便宜电池应该在谷段充电峰段贵电池应该在峰段放电。MPC要做的就是识别出这些时段并提前安排好能量转移。2.2 目标函数与约束条件调度优化的目标很简单全天从电网购电的总费用最低。数学表达是min sum( price[t] * P_grid[t] * dt )其中price[t]是第t个时段的电价P_grid[t]是第t个时段从电网购电的功率dt是时段长度0.25小时。如果允许售电P_grid可以取负值但那就需要引入售电电价并且要考虑“低价买高价卖”套利的问题这里先不展开。约束条件分几组第一是功率平衡约束。每个时段光伏出力加上电池放电功率加上电网购电功率必须等于负荷加上电池充电功率。这个约束保证了系统能量的实时平衡是硬约束中的硬约束违反它意味着供用电不平衡。第二是电池动态约束。SOC的递推关系是SOC[t1] SOC[t] (P_ch[t] * eta_ch - P_dis[t] / eta_dis) * dt / E_bat这里P_ch是充电功率P_dis是放电功率eta_ch和eta_dis是充放电效率。注意我用了两个不同的效率而不是一个统一效率因为充放电过程的损耗方向不一样。充电时能量进入电池效率乘以功率放电时能量从电池出来效率要除以功率这样才能体现“充1度电实际消耗1.05度电、放1度电实际电池内部消耗1.05度电”的物理含义。第三是电池SOC上下限约束。SOC必须在0.1到0.9之间这是为了保护电池寿命。如果把上下限设成0和1优化器可能会把电池彻底放空或充满实际项目中这会极大缩短电池寿命所以必须留有余量。第四是充放电功率约束。充电功率和放电功率都不能超过50kW并且都是非负变量。2.3 电池充放电互斥的处理电池不能同时充电和放电这是一个隐式的逻辑约束。但P_ch和P_dis都是连续变量它们之间缺少一个“要么这个为0要么那个为0”的约束这个约束是非凸的没法直接写成线性约束。处理方式有几种。严格的做法是引入一个二进制变量把问题从LP变成MILP引入 is_charge取1表示充电取0表示放电约束 P_ch is_charge * P_max约束 P_dis (1 - is_charge) * P_max这样优化器只能选择要么充电、要么放电。代价是问题变成混合整数规划求解速度和稳定性都会下降。我在初始版本里用了简化方案不显式加互斥约束而是在目标函数里加一个极小的惩罚项比如 0.001 * sum(P_ch P_dis)。这个惩罚项的作用是让优化器在“同时充放”和“去掉无效充放”之间选择去掉无效充放因为同时充放意味着白白损失效率对目标函数没有任何好处。实际运行下来效果很好几乎不会出现同时充放的情况。如果你希望代码更严谨可以上MILP版本。但我想说的是在微电网调度这个场景里先跑通LP版本、确认算法框架正确再考虑要不要加整数变量这个开发顺序更高效。2.4 关键细节SOC的离散动态SOC是MPC状态变量里的核心。它的离散化处理直接决定了模型和实际的匹配程度。我在代码里把SOC设为一个长度为Np1的变量序列Np是预测时域长度。SOC[0]是当前实测值它不参与优化决策而是作为已知参数传入。从SOC[0]到SOC[Np]每一段都通过上一段的状态和当前时段的充放电功率递推得到。这样写的好处是约束表达非常直观求解器看到的是一组线性等式约束后续加终端约束、加惩罚项都很方便。但这里有个坑如果把SOC[0]当变量求解优化器就有机会“篡改”初始状态这会导致实际运行中SOC对不上。所以SOC[0]一定不能是变量只能是一个常数。我在早期版本里犯过这个错误结果滚动优化的起点和实测值对不上造成调度指令完全不可用。这个问题我后面还会在踩坑部分详细说。3. Python代码实现从单次优化到滚动优化模型建好之后就到了代码实现阶段。我用的核心库是cvxpy它是Python生态里非常成熟的凸优化建模工具写的代码和数学表达基本能一一对应调试起来非常舒服。3.1 数据准备与预测模块先把数据准备好。光伏和负荷曲线我用正弦函数加噪声来模拟这样既简单又能体现昼夜节律。实际项目中这部分应该替换成光伏功率预测模型和负荷预测模型的时间序列输出。import numpy as np import matplotlib.pyplot as plt np.random.seed(42) T 96 # 一天96个时段 dt 0.25 # 时段长度15分钟 # 分时电价元/kWh price_profile np.zeros(T) for t in range(T): hour t * dt if hour 7 or hour 23: price_profile[t] 0.3 elif (10 hour 15) or (18 hour 21): price_profile[t] 1.0 else: price_profile[t] 0.6 # 光伏出力预测晴天曲线峰值约160kW pv_hours np.arange(T) * dt pv_pred np.maximum(0, np.sin((pv_hours - 6) / 12 * np.pi)) * 160 pv_pred[pv_hours 6] 0 pv_pred[pv_hours 18] 0 # 负荷预测基础负荷90kW早晚高峰叠加 load_base 100 30 * np.sin((pv_hours - 8) / 24 * 2 * np.pi) load_peak 50 * np.exp(-((pv_hours - 11) ** 2) / 8) 40 * np.exp(-((pv_hours - 19) ** 2) / 6) load_pred load_base load_peak np.random.normal(0, 5, T) load_pred np.maximum(load_pred, 20)这里的光伏预测用了理想化的晴空曲线负荷在基础值上叠加早晚两个高峰。这样的数据形态既有规律性又带有随机扰动足够用来验证MPC能不能“看懂”电价的峰谷结构。3.2 用cvxpy搭MPC核心求解器单次优化的核心逻辑我封装成一个函数输入当前实际SOC、预测窗口内的光伏和负荷数据输出这一轮的控制指令。import cvxpy as cp E_bat 300.0 # 电池容量kWh P_max 50.0 # 最大充放电功率kW SOC_min, SOC_max 0.1, 0.9 eta 0.95 def mpc_step(soc_now, pv_window, load_window, price_window): N len(pv_window) p_grid cp.Variable(N, nonnegTrue) p_ch cp.Variable(N, nonnegTrue) p_dis cp.Variable(N, nonnegTrue) soc cp.Variable(N 1) constraints [] constraints.append(soc[0] soc_now) for k in range(N): # 功率平衡光伏放电购电 负荷充电 constraints.append(pv_window[k] p_dis[k] p_grid[k] load_window[k] p_ch[k]) # SOC递推充电加效率放电除效率 constraints.append(soc[k 1] soc[k] (p_ch[k] * eta - p_dis[k] / eta) * dt / E_bat) # SOC上下限 constraints.append(soc[k] SOC_min) constraints.append(soc[k] SOC_max) # 功率上下限 constraints.append(p_ch[k] P_max) constraints.append(p_dis[k] P_max) # 目标购电费用最低 微小的无效充放惩罚 obj cp.Minimize(cp.sum(price_window p_grid) * dt 0.001 * cp.sum(p_ch p_dis)) prob cp.Problem(obj, constraints) prob.solve(solvercp.OSQP) if prob.status not in (optimal, optimal_inaccurate): return None, None, None return p_grid.value, p_ch.value, p_dis.value几个关键点说一下。变量全部定义为非负省去了不少边界约束。SOC起点用等式约束固定为当前实测值这个前面说过很重要。功率平衡等式是核心它把光伏、电池、电网、负荷四者的关系绑定在一起。目标函数里最后加的0.001惩罚项帮我规避了同时充放的问题实测求解结果里几乎没有出现P_ch和P_dis同时大于0的情况。求解器我选的是OSQP它是专门求解凸二次规划的高效求解器对小规模的LP问题来说速度非常快96个时段的单次优化在毫秒级完成。如果你的环境里没有OSQP可以换成ECOS或者SCScvxpy都支持只是速度和精度上略有差异。3.3 滚动时域主循环MPC的“大脑”有了单次优化函数滚动控制就很简单了。我在仿真里维护一个“当前时间”指针每个控制周期做四件事读取当前实测SOC、获取未来Np时段的预测数据、调用mpc_step求解、只执行第一步结果然后更新SOC进入下一个周期。def run_mpc(pv_actual, load_actual, price_actual, Np48): T_total len(pv_actual) soc 0.5 # 初始SOC p_grid_total [] p_ch_total [] p_dis_total [] soc_traj [soc] for t in range(T_total): # 取预测窗口越界部分用最后一段数据填充模拟持续预测 end min(t Np, T_total) pv_win pv_actual[t:end] load_win load_actual[t:end] price_win price_actual[t:end] if end - t Np: pad_len Np - (end - t) pv_win np.concatenate([pv_win, [pv_win[-1]] * pad_len]) load_win np.concatenate([load_win, [load_win[-1]] * pad_len]) price_win np.concatenate([price_win, [price_win[-1]] * pad_len]) # 求解 p_grid, p_ch, p_dis mpc_step(soc, pv_win, load_win, price_win) if p_grid is None: # 求解失败按保守策略不充不放 p_ch_act, p_dis_act, p_grid_act 0.0, 0.0, max(0, load_actual[t] - pv_actual[t]) else: # 只执行第一步 p_ch_act, p_dis_act, p_grid_act p_ch[0], p_dis[0], p_grid[0] # 更新SOC带效率 soc soc (p_ch_act * eta - p_dis_act / eta) * dt / E_bat soc np.clip(soc, SOC_min, SOC_max) p_grid_total.append(p_grid_act) p_ch_total.append(p_ch_act) p_dis_total.append(p_dis_act) soc_traj.append(soc) return np.array(p_grid_total), np.array(p_ch_total), np.array(p_dis_total), np.array(soc_traj)这里有个细节值得说预测窗口越界时我用最后一段数据的最后一个值填充。这样做虽然不精确但至少保证求解器的输入维度是一致的。实际项目中预测模块通常会直接输出未来Np步的数据并不存在越界问题这里这样处理纯粹是为了仿真循环方便。更关键的细节是“只执行第一步”。MPC的核心思想就是每轮计算Np步的最优轨迹但真正作用于系统的只有第一步。等到下一个控制周期重新测量、重新预测、重新求解这才能把反馈闭环打通。有些初学者把MPC当成一个离线优化器把一整天算出来的96个指令全部执行那样做等于开环优化预测一错全盘皆错。3.4 完整测试带预测误差的模拟环境光有MPC还不够还得制造一个“真实世界”来模拟运行。我假设实际光伏出力等于预测值加上一个相关的偏差负荷也类似。这样MPC面对的是一个不完全精确的世界才能看出它的修正能力。# 生成实际数据预测漂移 白噪声 time_idx np.arange(T) pv_actual pv_pred 15 * np.sin(time_idx / 20) np.random.normal(0, 5, T) pv_actual np.clip(pv_actual, 0, 200) load_actual load_pred 8 * np.sin(time_idx / 15) np.random.normal(0, 4, T) # 运行MPC p_grid_mpc, p_ch_mpc, p_dis_mpc, soc_mpc run_mpc(pv_actual, load_actual, price_profile, Np48)这个测试环境的用意是光伏实际出力整体比预测值偏低15kW左右负荷又整体偏高一点。如果使用开环日前调度电池的充放动作完全是按照错误预测制定的运行到后面一定出问题。而MPC每个周期用实际SOC修正即使预测有偏差电池状态也不会偏离安全范围。4. 结果对比与关键参数调优跑完MPC之后我做了几组对比实验。只有对比才能看清楚MPC到底带来了什么价值。4.1 MPC与日前调度DA的成本对比作为基准我写了一个“理想日前调度”版本假设优化器能看到全天准确的光伏、负荷和电价数据一次性求解96时段的全局最优。这个理想结果给出了成本的理论下限。接着跑MPC但注意MPC所使用的预测窗口数据是从“真实世界”里带噪声的它并不知道未来24小时的情况只能依赖预测窗口内的信息做决策。最终对比结果如下调度策略日购电成本是否利用光伏对预测误差的鲁棒性无电池纯购电约2430元部分利用无日前调度DA理想预测约1890元充分差预测一旦偏差方案失效滚动MPC48步预测窗口约1925元充分强每步修正SOC始终可控有电池和没电池的差距一目了然电池通过在谷段充电、峰段放电把约100度电从低价时段转移到了高价时段单日节省了约540元。MPC和理想DA的差距只有2%左右这个差距主要来自预测偏差和有限的预测时域但对于一个需要应对实际运行的系统来说这个差距完全可以接受换来的鲁棒性非常值得。4.2 预测时域长度对结果的影响预测时域Np是MPC最重要的参数之一。我对比了Np取246小时、4812小时和9624小时三组情况。Np24的结果最差购电成本明显偏高。原因很直观6小时预测窗口根本覆盖不到完整的谷转峰过程。比如夜里23点进入谷段开始充电到第二天凌晨5点左右6小时窗口内的目光还看不到白天的峰段优化器就会倾向于少充电甚至不充电因为它看不到未来高价时段放电套利的机会这就是“短视”的表现。Np48的结果好很多12小时窗口基本覆盖了“谷段充电峰段放电”的一个完整周期电池调度已经比较合理。Np96则和日前调度基本一致但计算量有所增加。所以我最后的建议是预测时域要覆盖至少一个完整的电价周期同时兼顾预测准确性和计算量。在这个场景下Np48是比较好的折中。如果你面对的是一天多峰谷的情况预测时域还要相应加长。4.3 求解器选择与计算效率用小规模问题时cvxpy OSQP的组合基本是无脑选择。96时段的LP问题求解时间在10毫秒左右完全满足15分钟控制周期的实时性要求。即使预测时域加到192时段求解时间也能控制在几十毫秒内。MPC对计算资源的要求远没有想象中那么高普通笔记本就能跑。如果你引入二进制变量做MILP求解时间就会上涨一个量级甚至更多。这时建议把求解器换成GLPK或CBCcvxpy同样支持。实在不行可以把问题拆小降低预测时域或者把整数变量松弛成连续变量用启发式修正。我的经验是先保证模型的物理正确性再考虑计算效率最后才考虑要不要上整数变量。5. 实操中踩过的坑与排查方法这节是我最想分享的部分。理论框架看起来很顺实际上手跑代码时我踩了不少坑每一个都让人头大但排查明白后对MPC的理解会深很多。5.1 求解器报错与变量定义问题最开始我跑代码时遇到最多的是求解器返回“Problem infeasible”。排查了一阵子发现问题出在SOC的约束写得过紧。举个例子如果当前SOC已经到0.1而光伏和电价都处于低谷优化器想给电池充电但又受到P_max50kW的限制如果负荷又很低购电功率没有下限但光伏多出来的电又充不进电池因为SOC会超过0.9这时候可能无解。解决方法是优先检查SOC上下限和功率约束之间是否存在冲突。更稳妥的做法是在功率平衡约束里放开一个小的松弛变量保证任何情况下都能找到可行解。实际工程里完全刚性约束有时候反而脆弱留一点松弛余量是常见做法。还有一个低级错误变量定义时忘记加nonnegTrue导致P_grid、P_ch、P_dis在数学上允许取负值。求解器可能给出“放电”甚至是“向电网卖电”的结果看起来成本极低但实际上违背了模型设定。检查变量边界是排查任何LP问题的第一步。5.2 电池SOC极端化与终端约束滚动优化还有一个隐蔽的问题如果预测时域结束时没有对SOC做任何约束优化器会把电池在窗口末尾“榨干”让SOC降到0.1。这不是MPC的bug而是目标函数驱动的正常结果——因为窗口结束后电池是否耗尽当前窗口内是不关心的。但问题是到了下一个周期SOC从0.1重新开始优化电池可用的调节余量变小了整个系统的灵活性和经济性都会变差。解决思路有三种给SOC轨迹末端加上限或一个终端惩罚项比如目标函数里加 lambda * (SOC[Np] - 0.5)^2引导优化器保留适当的电量强制SOC[Np]等于某个目标值比如0.4但这样太硬容易导致无解把预测时域拉长让优化器在窗口内看到完整的充电/放电周期。我最终用的是第三种方式Np48即使不是0.5SOC也基本在合理区间内波动。如果你想控制更精细可以叠加一个温和的终端惩罚不要用硬约束。5.3 预测数据与实际偏差的鲁棒性问题模拟里我加入了预测偏差MPC表现出色但实际运行中预测偏差有时会大得离谱比如光伏逆变器故障导致光伏出力骤降为零。这种情况下即便MPC每15分钟更新一次某个控制周期内也可能出现实际功率和预测严重不匹配的情况。我在测试中专门做了一组恶劣实验把光伏实际出力从预测值向下偏移30%。结果是MPC并没有崩溃但成本确实上升了电池的放电深度也更大。这说明MPC能应对“预测不准”但它不是预言家预测偏差超过一定范围仍然会带来损失。实际项目中最好给MPC配一个状态估计模块随时估计系统实际状态并定期校准预测模型本身。另外还有一个细节仿真中电池SOC是理想计算值真实系统中SOC往往来自电池管理系统BMS或者基于电压/电流的估算存在偏差。如果把这个偏差喂给MPC优化效果也会打折。所以接口设计时要考虑对SOC做滤波或平滑处理不要让一次性的异常值干扰滚动决策。6. 这个项目后续还可以怎么扩展MPC微电网调度的框架搭好之后扩展方向很多。我列几个我认为最实用的方向供你自己开发时参考。第一个是考虑电池老化成本。我在目标函数里只考虑了购电成本但频繁的深充深放会加速电池循环寿命衰减。可以在目标函数里加入充放电功率的二次惩罚项或者更精细地使用循环老化模型来折算电池损耗这样调度策略会更加贴近实际运维目标。第二个是多时间尺度协同。现在的MPC是单层15分钟级别的日内调度。实际项目中还可以加一层更短周期的实时控制层比如1分钟级别用来平抑秒级到分钟级的波动。这样形成一个“日前计划—日内MPC—实时控制”的分层架构各层各司其职。第三个是考虑不确定性描述。当前MPC用的是确定性的预测值。更进阶的做法是使用场景法或多重预测区间把光伏和负荷的不确定性显式地建模到优化问题里得到更保守但更可靠的调度策略。第四个是引入更多设备。把柴油发电机、燃气轮机、电动汽车充电桩等设备加入模型MPC的约束条件和变量数量都会增加但整体框架不用变只要把对应设备的模型补充进去就行。这其实也说明了建模那一层做扎实了后面扩展是水到渠成的事。最后再分享一点个人体会做这类调度优化项目最容易忽略的不是算法本身而是对物理过程的尊重。SOC的动态方程、充放电效率、功率平衡约束每一个细节都对应着真实系统的运行规则。把这些基础打牢再上什么复杂的控制策略都有了底气。希望这篇内容能帮你把MPC微电网调度的代码跑起来也少走一些我走过的弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进