微电网调度优化:基于MPC的滚动优化实践与Python实现
做微电网调度优化的人基本都遇到过这种尴尬中午光伏出力满格负荷却一路走低明明手里攥着预测曲线却不敢把储能充满因为根本不确定下午到底是要顶着阴天还是爆晴。我最早做这个方向时用的还是典型日前开环调度后来被一次预期内的大晴天狠狠坑了一把才真正把模型预测控制(MPC)的方法捡起来从头到尾用Python重写了一遍调度核心。这篇就把我在这套MPC微电网调度优化研究里的完整思路、建模过程、代码骨架和实测踩坑记录下来适合正在做微电网能量管理、或者想从传统调度切到滚动优化的朋友当作一份可以直接参考的实践笔记。1. 为什么微电网调度需要MPC从一次意料之外的弃光说起1.1 开环调度的两个死穴预测偏差与滚动失效传统微电网调度最典型的做法是提前一天做日前计划。基于第二天的光伏预测、负荷预测在满足供需平衡和储能约束的前提下离线求出一整天的储能充放电指令和电网购电功率曲线。这个方法逻辑上完全通代码跑通也容易但真实运行时问题很大。两个死穴。第一是预测偏差。光伏预测再准也有三五成以上的波动更不用说下午突然来一朵云出力直接腰斩。日前计划里储能该放的时候可能在充该充的时候可能在放一步错步步错。第二是零反馈。开环调度从头到尾只有一套计划无论实际运行状态偏离计划多远都不会有修正动作只能靠人工干预或者规则兜底。我那个项目里就遇到过一次预测是晴天中午把储能充电功率压低指望下午光伏满发再充结果下午云层压顶光伏出力只到预测的三分之一最后傍晚高峰只能高价买电当时最直观的感受就是——调度方案需要长眼睛。其实微电网调度面对的本质上是一个带不确定性的动态优化问题。光伏、风电、负荷都在随时变化而且未来几小时的信息谁也不能精确知道。面对这种环境一次性求出全天最优并不是好思路真正可靠的办法是走一步看一步每一小步都根据最新观测重新算这正是MPC的核心思路。1.2 MPC的思维转换预测、滚动与反馈三件套MPC说穿了就是三个词预测模型、滚动优化、反馈校正。它和开环调度的最大区别在于不追求一次性算对一整天而是在每一个采样时刻基于当前状态和未来一段时间的预测求解一个有限时域的优化问题然后把得到的控制指令只执行下一步等下一次采样再重新预测、重新求解。这个思路可以类比成开车从A地到B地。开环调度是出发前按地图和红绿灯信息把整个路径全部规划好之后无论路况怎么变都照着走。MPC则是手里拿着地图每到一个路口停下来看看当前的实时路况再以目的地为终点重新规划剩下这一段路。单个路口规划的路径未必是全程最优但时时修正下来反而更接近真实最优而且对突发情况适应极强。放到微电网场景里MPC每次求解使用的预测时域可能只有未来4到8小时而不是完整的24小时。这样做有两大好处一是预测时间短光伏、负荷预测的准确度大幅上升未来4小时的天气趋势比未来24小时的可靠得多二是每次优化都能纳入最新的测量反馈比如当前储能SOC、实际光伏出力、实时负荷不会让上一时刻的偏差一路滚到后面去。我的个人体会是把思路从开环调度切换到滚动优化难点不在数学而在心理你会非常不习惯每步只执行一个采样周期的指令总觉得浪费了后面算出来的一串最优解。但跑完仿真看到最终成本下降、弃光率下降之后这个疑虑自然就消除了。理解了这一点后面建模和编码就顺理成章了。2. 把微电网调度翻译成MPC能解的数学问题2.1 预测模型不需要复杂AI先用状态空间说清储能MPC落地第一步是建预测模型。这里的预测模型不是要求你去训练一个神乎其神的神经网络而是建立一套能根据当前状态和控制输入推算出未来一段时间系统轨迹的关系式。对于微电网调度核心对象是储能系统的SOC演变。储能单元的动态一般可以用离散状态空间方程描述SOC(k1) SOC(k) - (η_ch · P_ch(k) / C) · Δt (P_dis(k) / (C · η_dis)) · Δt其中SOC(k)是第k个采样时刻的荷电状态P_ch、P_dis分别是充电和放电功率η_ch、η_dis是充放电效率C是电池额定容量Δt是采样时间间隔。注意充放电功率的符号约定必须统一写代码时尤其要小心我经常看到有人在这里正负号没统一导致SOC反向漂移。光伏和负荷端不需要复杂的内部模型直接采用它们的预测序列即可。比如从k时刻开始未来Np步的光伏预测值为P_pv(k1|k)、P_pv(k2|k)……这些序列在调度机里可以是基于数值天气预报插值出来的也可以是基于历史相似日简单外推的MPC框架本身不挑食预测精度越高效果肯定越好但即使在一般精度下滚动机制也能兜底。2.2 目标函数运行成本最小化怎么取舍目标函数直接决定调度行为。我做的是微电网运行级调度当前阶段暂不考虑设备投资回收等远期成本目标聚焦在运行经济性上。以最小化调度周期内的总运行成本为目标典型构成包括从电网购电费用c_grid(t) · P_buy(t) · Δt储能充放电退化成本c_bat · (P_ch(t) P_dis(t)) · Δt弃光惩罚项可选c_curtail · P_curtail(t) · Δt用于引导系统尽量消纳光伏末端SOC偏移惩罚可选c_terminal · (SOC(Np) - SOC_ref)^2这几项怎么配权重非常考验对实际场景的理解。我的经验是先抓主要矛盾如果项目地电价峰谷差大、光伏渗透率高购电费用和弃光惩罚必须占主导如果储能投资成本高、循环寿命敏感退化成本就得压上去。末端SOC偏移项看似小却能防止MPC只盯着预测时域内省钱导致时域结束时电池几乎放空拖累下一轮滚动。目标函数写成累计加和的形式即可。如果希望求解器处理更高效建议整理成二次型比如把SOC偏移写成二次项MPC的优化问题就变成带线性约束的二次规划(QP)在Python里用成熟的优化库求解非常方便。2.3 约束条件功率平衡与储能边界约束是调度问题里最容易出软肋的部分少一条约束可能得到不可行的方案多一条不合理约束又可能把解空间压得毫无经济性可言。微电网MPC调度最基本的约束至少要有这些功率平衡约束P_pv(k) P_buy(k) P_dis(k) P_load(k) P_ch(k) P_curtail(k) ——光伏加上购电加上放电必须等于负荷加充电加弃光。这是整个模型的基石漏了它其他全白搭。储能SOC边界SOC_min ≤ SOC(k) ≤ SOC_max ——通常取0.1到0.9留出安全裕度过放过充都影响寿命。储能充放电功率限制0 ≤ P_ch(k) ≤ P_ch_max0 ≤ P_dis(k) ≤ P_dis_max ——PEM燃料电池、锂电池、飞轮储能的功率上限各有不同按设备铭牌给。电网购电功率限制0 ≤ P_buy(k) ≤ P_grid_max ——取决于并网容量或合同约定。弃光功率限制0 ≤ P_curtail(k) ≤ P_pv(k) ——弃光量不能超过当前光伏出力。所有约束要在预测时域的每一时刻同时写进去这是MPC和单点优化的本质区别。一个小技巧为了不让模型因为预测末端SOC逼近边界导致不可行可以对SOC边界做一点软化处理比如引入松弛变量让约束在极端情况下可以被轻微违反同时把违反量加进目标函数惩罚这样求解稳定性会好很多。这个我在第6节还会展开。3. Python代码实现从模型定义到滚动优化主循环3.1 基础数据准备光伏与负荷曲线我写MPC调度代码喜欢用cvxpy它的好处是建模语法接近于数学表达式读起来和学术论文里的公式一一对应调试时非常舒服。求解器底层可以用OSQP处理中小规模的QP问题速度快、内存占用小。开始编码前先准备两套数据序列一套是真实系统使用的实际光伏出力与负荷另一套是MPC内部滚动计算时使用的预测序列。两套数据有差异才符合现实否则就是开玩笑了。我在模拟项目X里是这样生成的真实序列用带随机波动的正弦叠加来做预测序列则在真实序列基础上加一个缓慢漂移的偏置模拟数值天气预报的系统性误差。import numpy as np import matplotlib.pyplot as plt import cvxpy as cp # 时间设置一天采样间隔15分钟 T 96 dt 0.25 # 小时 hours np.arange(T) * dt # 真实光伏出力kW峰值约100kW带波动 P_pv_real 100 * np.maximum(0, np.sin((hours - 6) / 12 * np.pi))**2 P_pv_real * 0.9 0.2 * np.random.rand(T) # 真实负荷kW早晚双峰 P_load_real 40 20 * np.exp(-((hours - 8) / 2)**2) 25 * np.exp(-((hours - 18) / 2.5)**2) P_load_real 3 * np.random.rand(T) # MPC内部预测序列在真实基础上加偏移 P_pv_pred P_pv_real (np.random.rand(T) - 0.5) * 10 P_pv_pred np.clip(P_pv_pred, 0, None) P_load_pred P_load_real (np.random.rand(T) - 0.5) * 5这只是数据骨架实际项目中预测序列应该来自正规预测模型但作为研究框架验证来说这种合成数据足够说明问题。3.2 优化问题定义用cvxpy搭一个凸优化模型每个采样时刻我们要解一个从当前k到kNp-1的有限时域优化问题。把受控变量定义为储能充电功率P_ch、放电功率P_dis、购电功率P_buy、弃光功率P_curt以及辅助的SOC状态变量。注意MPC里我们把SOC也当变量靠差分方程约束来串联各时刻。def solve_mpc_step(k, Np, Soc_now, pv_pred, load_pred, params): # 预测时域内的光伏和负荷子序列 pv_seq pv_pred[k:kNp] load_seq load_pred[k:kNp] # 变量 P_ch cp.Variable(Np) P_dis cp.Variable(Np) P_buy cp.Variable(Np) P_curt cp.Variable(Np) Soc cp.Variable(Np1) # 参数 C params[C] # 电池容量 kWh eta_ch params[eta_ch] eta_dis params[eta_dis] P_ch_max params[P_ch_max] P_dis_max params[P_dis_max] P_buy_max params[P_buy_max] Soc_min, Soc_max params[Soc_min], params[Soc_max] c_grid params[c_grid] # 购电价 元/kWh可分时 c_bat params[c_bat] c_curt params[c_curt] # 差分约束 constraints [Soc[0] Soc_now] for i in range(Np): constraints [ Soc[i1] Soc[i] (eta_ch * P_ch[i] - P_dis[i] / eta_dis) * dt / C, Soc_min Soc[i1] Soc_max, 0 P_ch[i] P_ch_max, 0 P_dis[i] P_dis_max, 0 P_buy[i] P_buy_max, 0 P_curt[i] pv_seq[i], pv_seq[i] P_buy[i] P_dis[i] load_seq[i] P_ch[i] P_curt[i] ] # 目标函数 cost 0 for i in range(Np): cost c_grid[i] * P_buy[i] * dt cost c_bat * (P_ch[i] P_dis[i]) * dt cost c_curt * P_curt[i] * dt cost params[c_terminal] * (Soc[Np] - params[Soc_ref])**2 prob cp.Problem(cp.Minimize(cost), constraints) prob.solve(solvercp.OSQP, verboseFalse) if prob.status not in (optimal, optimal_inaccurate): raise RuntimeError(fMPC优化失败: {prob.status}) return P_ch.value, P_dis.value, P_buy.value, P_curt.value, Soc.value这个函数体量和学术代码常规模块差不多但注意几个编码细节第一每一时刻的购电价c_grid是从外部按小时切片取进去的可以很自然地处理峰谷分时电价第二terminal SOC惩罚直接写在目标里避免了额外加终端等式约束导致过于刚硬第三SOC的上下限约束在设计上没有把第一时刻也强行约束在范围内只约束Soc[1]到Soc[Np]给求解器留了一点弹性。3.3 滚动主循环预测-求解-执行-更新MPC的核心运行循环说透了就是四步测量当前状态 - 获取未来预测 - 求解优化问题 - 只执行第一步指令。然后推进到下一个采样时刻重复以上步骤。这个循环结构基本上所有MPC代码都是同一骨架区别只是细节参数。def run_mpc(pv_pred, load_pred, pv_real, load_real, params, Np): T len(pv_real) Soc np.zeros(T1) Soc[0] params[Soc_init] P_buy_record np.zeros(T) P_ch_record np.zeros(T) P_dis_record np.zeros(T) P_curt_record np.zeros(T) for k in range(T): try: p_ch_k, p_dis_k, p_buy_k, p_curt_k, _ solve_mpc_step( k, Np, Soc[k], pv_pred, load_pred, params ) except RuntimeError: # 实际系统中此处会退化为规则策略兜底 p_ch_k, p_dis_k, p_buy_k, p_curt_k [0]*4 # 执行第一步 P_ch_record[k] p_ch_k[0] P_dis_record[k] p_dis_k[0] P_buy_record[k] p_buy_k[0] P_curt_record[k] p_curt_k[0] # 基于真实系统更新SOC Soc[k1] Soc[k] (params[eta_ch] * p_ch_k[0] - p_dis_k[0] / params[eta_dis]) * dt / params[C] Soc[k1] np.clip(Soc[k1], params[Soc_min], params[Soc_max]) return {Soc: Soc, P_buy: P_buy_record, P_ch: P_ch_record, P_dis: P_dis_record, P_curt: P_curt_record}主循环里有一个关键点是我当时纠结很久才想通的MPC求解出来的SOC轨迹是按预测序列演化的但真实物理系统的SOC必须按真实值更新。两者并不一致这是理所当然的——预测有误差SOC轨迹也会漂移。如果错误地用预测SOC作为下一步的起始SOC滚动框架的反馈作用就完全失效了。所以每步执行完控制指令后要根据真实光伏和负荷重新计算SOC作为下一轮优化的初始状态这正是MPC反馈校正的精髓体现。跑完主循环把充放电、购电、弃光记录画成曲线一个完整的MPC调度仿真链路就算跑通了。磨刀不误砍柴工建议第一次写时把这个循环跑在一组很小的人工构造数据上比如把预测序列设置成和真实完全一致先验证代码逻辑正确再引入误差。4. 预测时域与控制时域MPC最容易被忽视的旋钮4.1 时域参数对调度效果的影响MPC对两个时域参数极其敏感一个是预测时域Np一个是控制时域Nc。我最初做实验时照搬论文里的参数用Np24步对应6小时结果在日前的峰值负荷时段表现反而糟糕这时才意识到预测时域长短本质上是在长远眼光和预测精度之间做权衡。Np太短优化器视野放不远只会抓着眼前几个小时的局部最优猛打比如看到接下来2小时光伏充足就全力充电完全不管3小时后晚高峰来临时SOC已经被顶到上限无法继续放电Np太长问题规模线性增大求解时间随之上升而且越靠后的预测精度越差用不可靠的数据指导当前决策反而有害。从实际调试经验看微电网调度使用15分钟或1小时采样预测时域取4到8小时比较合理。如果采样15分钟Np取16到32步如果采样1小时Np取4到8步。控制时域Nc通常可以取和Np相同但在某些计算能力受限的场景里可以把Nc缩短——只优化前若干步的控制变量后面的控制变量保持恒定这样能显著减少决策变量个数。4.2 采样周期与计算开销的权衡采样周期对MPC的影响是双重的。周期越短对系统波动的响应越及时但Np不变的情况下预测的总时间长度就越短如果为了保持总预测时长不变而加大Np计算负担又上来了。工程上一般用15分钟到1小时15分钟适合波动剧烈的工商业微电网1小时适合变化平缓的园区或台区。求解时间也要实测。我这边用16步预测时域、大约20个决策变量的小问题OSQP求解平均耗时在几毫秒到几十毫秒之间即使在树莓派或工控机上跑也毫无压力。如果预测时域拉到96步一天变量数量翻几倍求解时间可能到几百毫秒对于一些控制周期是15分钟的系统完全没影响但如果控制周期压缩到秒级就必须考虑用更轻量的求解器或者把问题模型简化了。我个人的做法是写一个参数扫描脚本让Np从1到96步扫一遍记录每个Np下的总运行成本和求解耗时画成两条曲线。这个实验很有价值能直观看到边际收益递减的拐点在哪里为项目选参数提供了实打实的数据依据。5. 仿真结果解读怎么判断MPC真的跑赢了5.1 与开环日前调度的对比维度说半天效果好不好最终要落到数据对比上。我在评估MPC效果时会搭一个传统的日前开环调度作为对照组。开环调度用同样的目标函数和约束一次性求解24小时的充放电计划然后在仿真中按计划执行不闻窗外事。除了总成本至少要对比这几个维度对比维度开环日前调度MPC滚动调度总运行成本元高预测误差直接传导低滚动修正吸收偏差弃光率%偏高计划无法响应突变低可实时调整消纳SOC轨迹平滑度波动大容易出现极端值更平滑边界触碰更少末尾偏离参考SOC明显较小有终端惩罚项表格只是结果展示真正要理解的是为什么MPC在这些维度占优。以弃光率为例开环调度在中午时段如果计划储能充电功率较小光伏多余电量只能被白白切掉而且这个切掉是计划内的不会触发任何修正。MPC在每次滚动时都会看当前实际光伏出力如果发现比预测高出许多就会自动调大充电功率把这些多余电量吸收掉。这是机制层面的差别不是简单的参数调优。5.2 不确定性场景下的鲁棒性观察光靠一组正常天气仿真不足以说明MPC的优越性最好再把预测误差人为放大测试MPC的鲁棒性边界。我在模拟项目X里做过一组测试把光伏预测的偏置误差从±5%放大到±30%然后对比MPC和开环调度的运行成本上升幅度。结果是开环调度的成本随误差增大近乎线性恶化而MPC的恶化幅度明显平缓。原因也很好理解即便预测严重失准MPC每步都能重新决策误差最多影响未来几步的规划精度但永远不会累积成一天的错误执行。开环调度则完全相反一步预测错了全天跟着错。还有一个有意思的观察是SOC轨迹的末端行为。滚动优化每一轮都带terminal SOC惩罚可由于预测误差每轮结束时的SOC理论值和实际值总有一点偏差导致末端SOC会在参考值附近小幅抖动。如果抖动幅度让你不安可以把终端惩罚系数调大一点或者改用终端约束软化的方式但要注意别矫枉过正太强的末端拉回可能导致MPC牺牲太多经济性去保SOC。6. 实测踩过的坑与性能调优经验6.1 求解器选择和不可行问题处理cvxpy默认的求解器分流有点讲究如果问题规模小且约束严格用ECOS稳如果问题规模偏大OSQP是首选。我遇到过一个很隐蔽的问题初版代码里SOC差分等式写得太硬加上SOC上下限约束之后一旦预测序列极端糟糕比如连续几小时光伏预测为零、负荷预测又高得离谱优化问题直接返回infeasible整个仿真循环直接崩掉。处理不可行问题我的方案是两步走。第一是给储能SOC边界加松弛变量把Soc_min Soc Soc_max改成Soc_min - s1 Soc Soc_max s2松弛变量s1、s2为0到某个上限的非负变量并在目标函数里加大惩罚系数第二是留一个规则调度兜底分支一旦求解失败就用启发式策略临时接管避免系统失控。这两个机制合起来几乎能保证任何输入下仿真都能跑完对于做大量蒙特卡洛仿真的研究场景尤其重要。6.2 预测模型偏差与SOC漂移不管是仿真还是实际部署MPC最怕的不是单步误差大而是预测模型存在系统性偏移。光伏预测模型如果天天比实际出力低10%MPC就会天天把SOC往更高的方向充最后SOC顶着上限运行削峰能力大打折扣。纠正手段不是靠MPC本身而是定期做预测模型的校准或者在线更新预测模型的偏置参数。我在初版仿真中也碰到过SOC漂移问题滚动优化跑完一天SOC从初始值0.5漂到0.87。光看总成本是降下来了可这是把电池能量透支换来的第二天一开始就陷入低SOC窘境在长周期连续运行时根本扛不住。加terminal SOC惩罚之后逐日仿真居然能稳定在参考SOC附近这个小细节直接影响多日连续调度的可用性。6.3 代码层面的速度优化技巧MPC算法本身的计算量不大但如果做参数扫描、多场景蒙特卡洛仿真效率就重要了。我总结三个提速技巧第一cvxpy建模可以复用Problem对象每次滚动只更新参数值Parametrized Problem而不是重建整个优化模型速度能提升几倍第二用numexpr或numpy向量化构造约束数组避免Python循环里层层appendNp是32时没感觉Np是96时差异明显第三根据不同的求解器暴露的迭代次数、对偶间隙做诊断肉眼看到OSQP连续多次都不收敛时优先检查约束数值尺度是否统一——比如SOC按0-1标度功率按kW标度数值量级差太多时求解器很容易病态。最后分享一个小心得做MPC微电网调度研究仿真结果好看是第一步但真正能帮你判断算法能不能落地的是故意往系统里注入各种各样的坏数据。我每次都会把预测序列加上不同程度的污染、把负荷序列加入尖峰脉冲、把SOC初始值设在极端位置然后观察MPC在极端工况下是优雅降级还是直接崩溃。这套压力测试做完你对MPC的理解深度会和只跑一条正常曲线完全不在一层。拿这套代码继续往分布式协调、热电联产、甚至多微电网互联方向扩展滚动优化的骨架都是通用的核心要理解透的始终是预测、滚动、反馈这三者的协同配合。