在微电网调度中的Python实现与闭环优化)
前一阵子在复现一个模型预测控制MPC的微电网调度优化项目我把整套Python代码从传统的日前开环计划改成滚动优化结果发现代码本身不难难的是把问题的数学形式、时间尺度和工程边界想清楚。这篇就把MPC为什么适合微电网调度、优化模型怎么搭、Python代码怎么写成真正能跑的闭环循环完整记录一下。适合正在做新能源调度、储能EMS或者想入局电力优化方向的研究生和工程师参考。1. 微电网调度从一次性优化转向滚动决策的必然性1.1 为什么日前开环计划在微电网里容易翻车传统微电网调度最常见的方式是前一天晚上做一次24小时日前计划把第二天每个时段的储能出力、机组出力和并网功率全部定好第二天按计划执行。这个方法在负荷和光伏都很平稳的大电网场景里问题不大但放到微电网里就会非常难受分布式光伏出力受云层影响剧烈几分钟就能掉一半出力商业园区的负荷曲线在午休、上下班时段突变如果还参与了分时电价或现货市场价格信号也在波动。开环计划的问题是定了就不改。它本质上是基于某一版预测求解一个长时间尺度上的最优轨迹然后把它当作未来一定会发生的现实。可一旦预测偏差累积计划里的功率平衡约束就不再满足。我之前做过一个很典型的例子预测某天午后光伏峰值500kW计划里让储能傍晚放电结果上午云层遮光实际光伏只有300kW整个下午都在高价时段买电等傍晚负荷高峰来时储能早就没电了。整个日内计划从中午开始全面失真。用生活化一点的类比开环优化像出发前把全程路线、每个休息点精确到分钟的旅行攻略一旦堵车或天气变化整个计划就得推翻重来而滚动优化更像开车时的导航——只看前方几分钟的路况但每几秒重新计算一次始终在往目的地走。1.2 MPC与传统优化的本质区别只执行第一步模型预测控制MPC的基本思想是在每个采样时刻基于当前实测状态和最新的预测数据求解一个有限时域的最优控制问题得到未来N个时刻的控制序列但只执行当前时刻的第一个控制动作等到下一个采样时刻再拿到新的状态和新的预测重新求解。用数学语言说在时刻k求解min Σ_{tk}^{kN-1} [ 运行成本 状态惩罚 ] 终端惩罚 约束包括储能动态方程、功率平衡、SOC边界、功率限幅等。求解完得到控制序列 [u(k), u(k1), ..., u(kN-1)]但只把u(k)下发到执行机构。k1时刻到来后读取实际SOC和实际并网功率无论与预测有多大的偏差都作为新一轮优化的初始状态重新规划。这个只执行第一步的设计是MPC的灵魂。从控制理论角度它等价于在每个时刻都引入反馈把开环变成闭环从工程角度它让未来预测误差的影响只停留在当前这一步不会在后续时段无限制地累积下去。1.3 什么样的微电网场景最适合上MPC并不是所有微电网都需要MPC。如果系统里只有确定性负荷、柴油发电机长期满发、储能也只是一个定时充放的角色那用简单的规则表或者日前计划就够了。需要MPC的场景通常有这几个特征有光伏、风电这类强随机性的分布式电源且渗透率较高储能承担着削峰填谷、平抑波动、参与需求响应等复合功能电价或负荷曲线存在明显的日内变化调度决策需要看得更远一点才更经济主网交互容量有限需要频繁在买电和自发电之间做权衡。我在项目里用的是15分钟采样步长预测时域取12步也就是每次向前看3小时。这个时域长度既能覆盖午间光伏爬升、傍晚负荷高峰的形态变化又不会因为远期预测不可靠而让当前决策变得过度保守。2. MPC三件套预测模型、滚动优化、反馈校正怎么落地在微电网里2.1 预测模型精度取舍比算法更重要MPC里首先要有一个能描述系统未来演化的模型。对微电网调度来说预测模型分两层一层是设备动态模型主要指储能SOC的递推关系另一层是扰动预测包括负荷、光伏出力、电价在未来N步的取值。很多初学者在预测上花大量精力恨不得用深度学习把光伏预测做到完美。但实际上MPC对预测精度的要求比想象中低——因为反馈校正会不断修正偏差预测只要把趋势形状给对比如中午有光伏峰、傍晚有负荷峰、晚高峰电价高就足够让优化器做出合理的储能充放决策。我在仿真里生成负荷用的是基础曲线 随机噪声光伏用早晚低、中午高的高斯形态电价则直接给分时峰谷。之后在测试阶段故意往光伏预测里叠加±25%的误差MPC依然能把成本控制在可接受范围。这其中的关键不是预测有多准而是滚动重规划让误差只作用于当前步。这里要多说一句设备动态模型反而更需要准确。储能SOC递推、效率系数、功率限幅这些一旦建模错误反馈校正根本救不回来。比如把充放电效率写反SOC会出现越充越少的诡异现象这种问题在代码调试阶段非常隐蔽。2.2 滚动优化有限时域是刻意为之有限时域N的选取值得好好琢磨。取太大比如直接取96步24小时每一轮求解变量变多、计算变慢而且远期预测已经不可信相当于把日前计划塞进MPC的壳里取太小比如只用3步优化器就看不到傍晚的负荷高峰储能在白天就可能过早放空。我常用的经验值是步长15分钟N取8到24对应前瞻2到6小时。对于大部分园区级微电网这个参数能让储能既服务于短时波动平抑又能响应日内峰谷价差计算开销在cvxpy加开源求解器下也就是几十毫秒。滚动优化还有一个容易被忽略的优点它天然支持约束逐时更新。比如某时段主网下发限功率指令MPC可以把这个约束直接加进下一轮求解而没有反馈机制的开环计划只能事后补救。2.3 反馈校正状态更新决定了MPC的天花板MPC和开环优化的最大差别不在预测而在反馈。每个采样周期结束后我们需要拿到两个真实值储能实际SOC以及并网口实际功率。下一轮优化把这两个值作为初始状态和当前时刻的平衡校验基准让模型误差被不断拉回。仿真里SOC是模型算出来的看起来没有估计问题但真实系统里SOC通常靠库仑计累加或状态观测器估计存在积分漂移。如果反馈值本身不准后面所有优化决策都会受到污染。所以我做仿真时会把SOC实测值故意叠加一个缓慢漂移的噪声用来检验控制器在这种反馈偏差下是否还能稳定工作。结果发现只要SOC误差不超过5%MPC的性能几乎没有明显下降一旦估计偏差超过10%储能的充放节奏就会明显错乱。从这个角度看反馈校正的质量决定了MPC性能的上限。预测模型可以糙一点状态观测不能糙。3. 调度优化模型怎么建约束、变量、代价函数逐一拆开讲3.1 决策变量把可控部分和不可控部分分开建立优化模型第一步是分清哪些是决策变量哪些是扰动输入。在微电网MPC里我习惯这样划分可控决策变量储能充放电功率P_b、可调度机组出力P_g如果有柴油机或燃气机、并网交互功率P_grid状态变量储能SOC由P_b的积分决定不可控扰动负荷功率P_load、光伏出力P_pv、电价price这些来自预测模块作为外部参数传入优化问题。并网功率到底是决策变量还是平衡量取决于问题设定。如果微电网与主网可以自由买卖电那么P_grid应该作为决策变量参与优化如果主网只提供一个固定的容量上限P_grid则可以在功率平衡约束里被反算出来。做研究时我倾向于把P_grid显式建为决策变量并加上联络线功率上下限约束这样后续分析并网峰值、评估主网支撑水平都很方便。一个重要细节是符号方向统一我约定P_b 0表示储能放电P_b 0表示充电P_grid 0表示从主网购电P_grid 0表示向主网购电或反送。这个约定必须在整个模型里从头贯彻否则功率平衡约束的正负号会把人折磨到怀疑人生。3.2 约束条件功率平衡、SOC与线路容量的数学写法把约束用表格列出来几年的经验全在这张表里约束名数学表达式说明功率平衡P_b(k) P_g(k) P_grid(k) P_pv(k) P_load(k)每个时步发用电瞬时平衡SOC动态SOC(k1) SOC(k) - η·P_b(k)·Δt / E_rated储能能量状态演化SOC限幅SOC_min ≤ SOC(k) ≤ SOC_max防止过充过放保护寿命充放电功率限幅-P_ch_max ≤ P_b(k) ≤ P_dis_max逆变器与电池能力边界机组出力限幅P_g_min ≤ P_g(k) ≤ P_g_max可调度机组出力范围机组爬坡约束|P_g(k) - P_g(k-1)| ≤ R_g机组调节速率限制并网容量约束P_grid_min ≤ P_grid(k) ≤ P_grid_max联络线传输能力功率平衡是硬约束代表能量守恒。只要决策变量划分正确这一条在数学上是等号约束不需要也不应该留松弛空间。真正容易出问题的是SOC动态里的效率系数。效率的处理是个大坑充放电效率如果不同SOC递推公式会变成带符号的分段函数这在线性MPC里没法直接写成表达式。学术论文里常用两种妥协方案一种是统一使用一个往返效率η公式写成SOC(k1) SOC(k) - P_b(k)·Δt / (η·E_rated)另一种是分别用充电和放电两个效率系数但只做线性近似。如果一定要精确建模就得引入二进制变量把充电和放电两个工况分开问题从QP变成MILP求解时间明显增加。在我的仿真里为了保持代码简洁且能跑得动采用统一效率η0.9同时在结果分析中明确指出这个简化对成本评估带来的偏差方向——它会让储能动作显得比实际略微划算因为实际电池充放电总有损耗。3.3 代价函数运行成本、SOC终端惩罚与权重归一化目标函数是MPC设计里最考验工程直觉的部分。最基础的形式是经济型目标函数J Σ_{k0}^{N-1} [ price(k)·P_grid(k)·Δt fuel_cost·P_g(k)·Δt ] λ·(SOC(N) - SOC_ref)²第一项是从主网购电的成本第二项是燃料成本最后一项是终端SOC惩罚。如果微电网还考虑了储能退化成本可以再加一项β·P_b(k)²用来抑制过于频繁的充放电切换。为什么一定要有终端惩罚这是我在复现研究时踩得最狠的一个坑。如果没有最后一项优化器会倾向于在整个预测时域末尾把储能放到最低限因为尽量少买电、多用储能在当前时域内是最省的。这会导致每天末尾SOC掉到下限第二天一开始就没有可用的储能缓冲。终端惩罚的作用是用一个虚拟代价告诉优化器这个时域结束后的剩余电量是有价值的别把它用尽。权重怎么定更是一门学问。price、fuel_cost、SOC_ref的单位都不一样有的量纲是元/kWh有的是无量纲比例如果直接加权量级大的项会彻底淹没量级小的项。我的做法是把所有功率量纲统一成标幺值以并网容量为基准SOC本来就是0到1的无量纲数再对三个代价分量分别归一化到同一数量级最后用权重系数调节优先级。这样调参时每个权重都有直观意义。3.4 一个完整的MPC数学表达把上面内容汇总每个时刻k求解的问题可以写成min Σ_{tk}^{kN-1} [ c_grid(t)·P_grid(t) c_fuel·P_g(t) β·P_b(t)² ] λ(SOC(kN) - SOC_ref)²s.t. P_b(t) P_g(t) P_grid(t) P_pv(t) P_load(t)SOC(t1) SOC(t) - η·P_b(t)·Δt / E_ratedSOC_min ≤ SOC(t) ≤ SOC_max-P_ch_max ≤ P_b(t) ≤ P_dis_maxP_g_min ≤ P_g(t) ≤ P_g_maxP_grid_min ≤ P_grid(t) ≤ P_grid_max初始条件SOC(k) SOC_measured这个问题的输入是当前实测SOC和未来N步的P_load、P_pv、price预测输出是一串控制序列提取第一个P_b和P_grid去执行。每次滚动都重新求解这就构成了完整的闭环MPC。4. Python实现细节从cvxpy建模到闭环滚动主循环4.1 技术选型为什么用cvxpy而不是scipy做MPC求解代码层面最重要的选择是优化建模库。我最终选了cvxpy配CLARABEL求解器理由很简单cvxpy能用接近数学公式的语法声明变量、约束和目标函数可读性极强代码和论文里的公式几乎一一对应CLARABEL和OSQP都是免费开源求解器对中等规模的凸优化问题求解速度足够快。有些人习惯用scipy.optimize.minimize加SLSQP这在变量少、约束简单时也能跑但坑在于你需要手动把约束写成函数形式目标函数和约束的梯度信息全靠数值微分一旦问题规模变大就非常慢而且对等式约束的处理也不够稳健。相比之下cvxpy直接把线性动态、二次目标交给底层的内点法或ADMM求解器几十个变量的MPC求解时间通常在几十毫秒到几百毫秒完全满足15分钟采样间隔的实时性要求。还有一个原因是可扩展性。后续如果从确定性MPC升级到场景随机MPC或者加入二进制变量做更精细的充放电效率建模cvxpy的语法依然能承载不需要推倒重写。4.2 仿真数据结构与类设计我的代码不是把所有逻辑堆在一个脚本里而是拆成几个职责清晰的模块MicrogridParams用dataclass保存微电网参数包括储能容量、功率限幅、效率、SOC边界、并网容量、采样间隔ScenarioGenerator生成负荷、光伏、电价曲线支持叠加噪声来模拟预测误差也支持传入外部真实数据MPCScheduler核心控制器内部用cvxpy构建优化问题提供solve方法接收当前SOC和未来N步预测返回第一步控制动作Simulator模拟被控对象按照真实动力学推进接收控制动作后更新SOC、记录功率轨迹同时可以注入扰动。这种设计的价值在于MPC控制器和被控对象是解耦的。仿真里我可以让Simulator使用真实的光伏出力和负荷而MPCScheduler只能看到带误差的预测这样才叫真正的闭环验证。很多初学者的代码让控制器和仿真共用同一套数据结果反馈校正形同虚设性能再漂亮也没有参考价值。4.3 MPC核心代码问题建模、约束、求解核心代码不长但每个环节都很关键。下面是我整理出的可运行骨架参数用小规模微型电网示例import cvxpy as cp import numpy as np # ---------- 系统参数 ---------- E_rated 100.0 # 储能额定容量 kWh SOC_min, SOC_max 0.2, 0.9 P_ch_max, P_dis_max 30.0, 30.0 # 储充/放功率上限 kW P_grid_max 200.0 # 并网功率上限 kW eta 0.9 # 储能往返统一效率简化 dt 0.25 # 采样间隔 15min # ---------- 预测序列由ScenarioGenerator提供 ---------- N 12 P_load np.array([...]) # 未来N步负荷预测 P_pv np.array([...]) # 未来N步光伏预测 price np.array([...]) # 未来N步电价 P_g_max 50.0 fuel_cost 1.2 # 燃料成本 元/kWh # ---------- 决策变量 ---------- P_b cp.Variable(N) # 储能功率正为放电 P_g cp.Variable(N) # 机组出力 P_grid cp.Variable(N) # 并网交互正为购电 SOC cp.Variable(N 1) # SOC序列 SOC_ref 0.5 lambda_term 100.0 beta 0.01 constraints [ SOC[0] 0.5, # 当前实测SOC ] for k in range(N): # 功率平衡 constraints.append(P_b[k] P_g[k] P_grid[k] P_pv[k] P_load[k]) # SOC动态统一效率线性模型 constraints.append(SOC[k1] SOC[k] - P_b[k] * dt / (eta * E_rated)) # 储能限幅 constraints.append(SOC[k1] SOC_max) constraints.append(SOC[k1] SOC_min) constraints.append(P_b[k] -P_ch_max) constraints.append(P_b[k] P_dis_max) # 机组与并网限幅 constraints.append(P_g[k] 0) constraints.append(P_g[k] P_g_max) constraints.append(P_grid[k] -P_grid_max) constraints.append(P_grid[k] P_grid_max) # 爬坡约束与上一时刻关系第一时刻用当前实际值 if k 0: constraints.append(cp.abs(P_g[k] - P_g[k-1]) 10) # ---------- 目标函数 ---------- cost 0.0 for k in range(N): cost price[k] * P_grid[k] * dt cost fuel_cost * P_g[k] * dt cost beta * P_b[k] ** 2 cost lambda_term * (SOC[N] - SOC_ref) ** 2 prob cp.Problem(cp.Minimize(cost), constraints) prob.solve(solvercp.CLARABEL) # 取出当前时刻控制动作 P_b_action P_b.value[0] P_grid_action P_grid.value[0]这里有三个细节需要说明。第一SOC[k1]的约束直接写成硬限幅如果求解器报告无解我建议把SOC边界改成软约束或者干脆在目标函数里加SOC越限惩罚这个问题后面专门讲。第二统一效率模型用除法把效率折进放电过程但严格来说充电和放电损耗方向不同演示代码够用工程精度不够。第三P_g的爬坡约束会占用较多求解时间如果机组调节速度很快可以从模型里删掉。4.4 闭环主循环与常见时间对齐错误有了MPCScheduler闭环主循环就非常简单了T 96 # 仿真一天15min一个点 soc_actual 0.5 history [] for k in range(T): # 控制器只能看到带误差的预测 load_fc forecast_load(k, N) noise() pv_fc forecast_pv(k, N) noise() price_fc get_price_forecast(k, N) action mpc.solve(soc_actual, load_fc, pv_fc, price_fc) p_b_exec action[P_b] p_grid_exec action[P_grid] # 真实世界动态使用真实负荷/光伏可能还有实际功率限幅 p_b_real apply_actuator_limit(p_b_exec) soc_actual soc_actual - p_b_real * dt / (eta * E_rated) soc_actual np.clip(soc_actual, SOC_min, SOC_max) history.append((p_b_real, p_grid_exec, soc_actual))这个循环里最容易犯的错误是时间对齐。预测序列必须从当前时刻开始、长度为N而不是随便从数据集里取一个切片。更隐蔽的问题在于如果使用的是历史数据的索引Simulator推进一步后下一次的当前状态必须来自仿真结果而不是重新读表。我见过不少代码把仿真状态和控制器的测量值用成了同一条曲线导致反馈环节被人为关闭整个MPC实际退化成开环。一个实用技巧是所有与时间相关的变量都带上明确的k标注并在每次循环末尾用assert确保SOC的更新来自Simulator的返回值而不是控制器的内部变量。5. 实验结果怎么看与基线对比、扰动测试和经典坑位5.1 三组对比实验成本、峰值、SOC健康度只跑一条MPC曲线说明不了问题必须和基线方案对比。我设计了三个对照组日前开环优化用同样的模型一次解24小时然后整个计划都不再更新、MPC滚动优化、固定规则策略光伏优先、储能低谷充满、高峰放出。评价指标选四个总运行成本、并网峰值功率、储能等效循环次数、SOC越限时间。成本直接反映经济性并网峰值关乎容量费用SOC越限体现安全性充放电次数侧面反映电池寿命。在一个典型夏日的仿真中我的测试结果大致是预测完美时MPC和日前优化的成本几乎一样差距在1%以内因为两者的决策基础相同叠加预测误差后MPC的成本比开环计划低10%左右并网峰值功率也有明显下降原因是储能动作总是基于最新信息不会被错误计划锁死。这个结论想说明的是MPC的最大收益不是来自比开环聪明而是来自允许犯预测错误之后继续修正。如果预测已经完美滚动优化带来的额外收益本来就有限。5.2 预测误差扰动下的鲁棒性验证为了更系统地验证鲁棒性我给光伏预测添加了±25%的随机误差负荷预测加±10%的误差然后在同一组真实曲线下重复仿真20次统计成本分布。开环计划的成本方差明显更大最差情况下比理想成本高出25%以上MPC的成本方差很小平均成本比理想成本高出大约8%。原因还是那条MPC每一时刻都在重新决策单步预测错误只影响当前步而开环计划的错误会沿着时间轴一路传导到计划末尾。我在做这组实验时特意把随机种子固定并输出每次仿真的成本明细方便复盘。如果你也想在论文里用这个结论建议不要把一次仿真作为全部依据至少要做扰动分布的蒙特卡洛评审和审稿人都会看趋势稳定性。5.3 优化问题无解、SOC漂移、效率符号三座大山这一节想专门记录我在复现过程中踩过的三个大坑。第一个坑是优化问题无解。当预测误差较大时SOC边界、功率平衡、并网限幅可能同时卡得很紧硬约束组合在一起会冲突。比如光伏预测高了实际出力低而储能又处于SOC下限功率平衡约束可能靠现有手段无法满足。我的解决方案是给功率平衡加一个松弛变量slack cp.Variable(N, nonnegTrue) constraints.append(P_b[k] P_g[k] P_grid[k] P_pv[k] slack[k] P_load[k]) cost 1000.0 * cp.sum(slack)这本质上是把硬约束变成软约束只在极端情况下牺牲功率平衡换取消求解成功。第二个坑是SOC末端漂移。前面提过目标函数漏掉终端惩罚会让每天的末尾电量贴着SOC下限跑。更隐蔽的形式是加了终端惩罚但系数太小仍然挡不住成本项的吸引力。我最后把λ调到了一个足以让SOC偏离1个百分点就产生与购电成本同数量级惩罚的数值才稳定下来。第三个坑是效率符号错误。统一效率模型里我一开始写成了SOC(k1) SOC(k) P_b·dt/(η·E_rated)正负号完全反了结果放电时SOC反而上升。这类bug不会报错只能通过绘制SOC曲线一眼看出来。建议每次跑完都先画SOC和功率的时序图观察放电时SOC是否下降、充电时是否上升再谈优化结果。6. 从仿真到工程落地的几点个人体会6.1 仿真能跑不等同于现场能用我在把同一套MPC代码接到实际项目里时感触最深的是仿真里的理想闭环和现场的物理闭环之间隔着通信时延、执行器死区、SOC估计误差和预测接口不稳定四堵墙。仿真里一条步进语句就能拿到真实SOC现场SOC却要靠BMS上报可能延迟、可能跳变、可能带偏置。所以我的原则是仿真阶段就主动给控制器喂带误差的状态和带噪声的预测让代码的鲁棒性在开发期就被迫练出来。具体做法是给SOC测量值叠加一个缓慢漂移信号给功率执行值加一个随机误差。你会发现一个在完美测量下运行良好的MPC在这些小扰动下可能会频繁触发功率限幅这才是真正需要优化的地方。6.2 下一步演进随机MPC、分布式MPC与数据驱动预测如果只把确定性MPC做完其实已经解决了微电网调度的主要工程问题。再往研究方向发展个人认为有三条比较自然的路径。第一条是随机MPC把预测不确定性显式地用场景集合表达每个场景对应一条负荷和光伏曲线优化目标变成多个场景下的期望成本。好处是控制器会主动为最坏情况留出储能余量代价是问题规模和求解时间直线上升。第二条是分布式MPC面向多个微电网或楼宇间协调场景。每个子问题只优化自己区域内的变量然后通过相邻区域通信交换边界功率信息迭代收敛到全局较优解。这套思路和中央集中式优化相比最大的好处是隐私保护和故障隔离。第三条是数据驱动预测用机器学习预测光伏和负荷再接入MPC框架。但我的经验是预测精度的提升对MPC效益的边际贡献会随着反馈校正的存在而递减优先把时间花在状态估计和约束处理上往往回报更高。6.3 几条能直接用的实操建议最后分享几个我实测过、可以节省大量调试时间的做法。预测时域N不是越大越好。我常用N8到24步长15分钟在性能和时间开销之间比较平衡。N越大求解越慢远期预测也越不可靠反而让当前动作变得畏手畏脚。cvxpy的Problem对象不要在每个采样周期重新创建。可以把问题构建放在初始化阶段每个周期只更新参数值并重新solve能省下不少建模开销。如果仿真规模大这个优化会让总运行时间明显缩短。所有成本项的权重都要先归一化再调参。我的习惯是先把功率转换为标幺值目标里各项量级压到0.1到10的区间里再根据效果微调。这样至少不会出现储能项权重太小所以从不动作或者SOC惩罚太大导致储能在没有利润空间时也频繁充放这类玄学问题。并网功率上限这种约束我建议在正式实验里设成软约束留一个很小的松弛量。真实系统中联络线容量是硬极限但调度算法探测到边界时优先保证系统有解、有可行动作比红外灯守住一个不留余量的硬等式重要得多。这套MPC代码跑通之后换到不同微电网参数只需要改MicrogridParams里的几个数字。如果你也正在这个方向上折腾希望这篇记录能帮你少踩几个我踩过的坑。