微电网里最磨人的不是电池本身,而是把“用不完的电存起来还是卖给电网、缺电的时候买电还是让柴油机顶上”这笔账算清楚。最近在做微电网调度优化的课题,方向就是基于模型预测控制(MPC)做日内滚动调度,编程语言用Python,前后跑了三周才把闭环彻底跑稳。这篇文章就把整套思路、数学模型、Python代码结构以及我踩过的坑一次性讲清楚。
这套内容适合谁看?一种是做储能EMS算法、微电网科研实验的同学,另一种是开始接触能源调度的工程师。你不需要提前学过MPC,只要会基本的Python和numpy操作,按文章的思路就能把一条可以跑通的微电网调度优化链路搭起来。文章侧重工程实现和调参经验,不是把教材复述一遍,很多细节是代码注释和论文里不会写的。
1. 为什么微电网调度要用MPC:问题拆解与方案选型
1.1 微电网调度到底在调度什么
微电网可以理解成一个小型电力系统:光伏、风机这些分布式电源,加上储能电池、可调的柴油发电机,以及一群负荷,通过一个公共连接点跟外部电网交换功率。调度的意思,就是在每个控制周期里决定储能是充还是放、放多少,柴油机出力多少,从电网买多少电,或者反过来能不能卖电。
这件事的难点在于,做决定的那一刻,你并不知道未来5分钟光伏出力会不会骤降、负荷会不会突然爬升。调度本质就是在“不确定的未来”和“现在必须行动”之间找平衡。
时间尺度这里要先说清楚。微电网调度一般分三层:日前计划(24小时,小时级)、日内滚动(15分钟到1小时一个周期,预测未来4到24小时)、实时控制(秒级到分钟级)。MPC主要承担日内滚动这一层,因为它天然支持“每个周期重新计算一次计划”。这是我选定这个方向的最主要原因。
不确定性的来源主要有三类:光伏出力的间歇性、负荷的随机波动、以及市场电价的浮动。这些不确定性叠加起来,如果只做一次开环优化然后照着执行,误差会越滚越大。MPC的滚动优化加反馈校正,恰好把这条路堵住了。
1.2 对比几种常见方案:为什么是MPC
做这个课题之前,我把几种主流调度策略都对比过一遍,大概情况如下:
| 方案 | 基本思路 | 优点 | 缺点 | 工程适用性 |
|---|---|---|---|---|
| 规则控制 | 看SOC、时间、电价做if-else判断 | 实现简单,稳定 | 非全局最优,经济性差 | 小型系统、紧急备用 |
| 日前开环优化 | 用预测做一次全局优化 | 理论全局最优 | 对预测误差非常敏感 | 预测精度极高的理想场景 |
| 动态规划 | 枚举状态转移求最优 | 理论全局最优 | 状态维度爆炸 | 小规模状态空间 |
| 强化学习 | 通过试错学习策略 | 无需精确模型 | 训练慢,可解释性差 | 数据丰富、算力充足 |
| MPC | 滚动优化加反馈校正 | 兼顾最优性与鲁棒性 | 需要预测模型和求解器 | 微电网日内调度主线方案 |
为什么最终选了MPC?理由其实比较朴素。微电网调度本质上是一个带约束的在线决策问题,MPC就在这个框架内工作:把各种运行约束直接写进优化问题,把不确定性交给滚动修正。开环优化对模型精度要求太高,动态规划状态一多就容易爆炸,强化学习训练成本高而且决策过程不透明。对课题验证和工程落地来说,MPC都是最顺的那条路。
1.3 MPC在微电网场景中的核心优势与代价
说几个实际跑下来的感受。第一,MPC处理约束的能力很强。SOC上下限、充放电功率上限、变压器容量限制,这些都能作为硬约束写进优化问题,求解器会自动避让。第二,反馈校正这个能力非常关键。等真正到了控制时刻,我会用实测SOC重新初始化状态,而不是接着上次预测值继续算,这样预测误差不会无限累积。第三,多目标很容易平衡,比如在成本目标里加上功率波动惩罚,避免设备频繁动作,这在传统规则控制里几乎是没法加的。
代价也要说清楚。MPC不是银弹。首先你得有一个可用的预测模型,哪怕是简单的持续预测法(用当前光伏出力当作未来几小时的值)或AR自回归,也比没有预测强。其次,每个控制周期都要调用求解器解一次优化,对这种小规模问题毫秒级就能解完,但如果未来扩展到几十个节点,实时性需要重点考虑。最后,参数整定相当考验经验:优化时域选多长、权重怎么分配、要不要加惩罚项,这些直接决定结果好坏。很多人跑了一两轮发现MPC效果不理想,大部分情况不是算法不对,而是参数没调明白。
2. MPC微电网调度数学模型搭建
2.1 状态、控制与预测模型
建模第一步是把系统抽象成状态空间表达式。对微电网日内调度这个场景,最常见的做法是:
- 状态变量:储能SOC、当前时段编号(涉及分时电价时尤其有用)。
- 控制变量:储能充放电功率、柴油机出力、与电网的交换功率。
- 扰动变量:光伏出力、负荷功率、电价序列,这些来自预测模块。
以储能为例,离散状态方程写作:
SOC(k+1) = SOC(k) - [ P_charge(k) / ηc - P_discharge(k) × ηd ] × Δt / E_cap
其中P_charge和P_discharge都是非负变量,这样充放电效率可以分别建模。这里有一个关键细节:分开定义充放电变量后,如果在目标函数里不做约束,求解器有可能出现同时充电又放电的情况来“骗”目标函数,这会导致结果毫无物理意义。解决思路有两种。
第一种是显式加互斥约束:P_charge × P_discharge = 0,或者引入一个二进制变量b,让充电时P_charge只能为正、放电时P_discharge只能为正,用大M法把互斥关系线性化。第二种是靠在目标函数中设计合理的成本项来抑制,比如购电成本、电池老化成本,使得同时充放只会白白增加成本和损耗,求解器自然不会那么干。
我在实际演示代码里用的是第二种,注意前提条件是售电价格低于购电价格,且电池老化惩罚不为零,否则理论上有套利空间。如果是做严格研究,建议把互斥约束老老实实写进去,不要省这一步。
2.2 优化目标:经济性还是舒适度?
目标函数决定了MPC更看重什么。微电网调度最普遍的目标是经济性,也就是最小化一个调度周期内的总成本。一般形式是:
min J = Σ [ 购电成本 - 售电收益 + 燃料成本 + 电池老化成本 + 功率波动惩罚 ]
逐项拆开看:
- 购电成本:从电网买电的结算费用,按分时电价计算。
- 售电收益:光伏大发时向电网卖电的收入,仅在允许倒送时存在。
- 燃料成本:柴油机发电的油耗成本,通常用二次函数近似。
- 电池老化成本:储能每完成一次充放循环都有寿命损耗,可以简化为与功率成正比的惩罚项。
- 功率波动惩罚:限制储能输出变化率,降低设备机械疲劳,这在工程中很重要但常被论文忽略。
这些项的量纲差异很大,权重设置非常关键。我的建议是先把经济项单独拎出来算一遍基准成本,然后给波动惩罚一个相对较小的权重,比如经济成本的1%到5%,之后根据曲线再微调。头铁一次性同时调五个权重,基本会掉进调参黑洞。另一个需要注意的点是,所有项都要归一化到同一时间尺度,比如都是每小时成本。
2.3 约束条件:边界与瓶颈
优化问题能不能解出来,约束写没写全是第一关键因素。下面列的是我在单储能场景下最常用的约束集:
- 功率平衡约束(核心等式约束):负荷 + 充电功率 = 光伏出力 + 放电功率 + 购电 - 售电。这个等式必须严格成立,否则系统不平衡。
- 储能SOC约束:SOC_min ≤ SOC(k) ≤ SOC_max。如果做长时间仿真,末时段SOC还要拉回初始值附近,否则第二天没有足够的电量可用。
- 储能功率约束:0 ≤ P_charge(k) ≤ P_charge_max,0 ≤ P_discharge(k) ≤ P_discharge_max。
- 柴油机约束:出力上下限,以及爬坡约束,即相邻时段出力变化不能太大。
- 电网交换功率约束:购电和售电功率各有上限,否则求解器可能会为了满足某个软目标而无限买电。
还有一个隐蔽的坑:约束顺序。cvxpy建模时先声明变量,再逐条添加约束,变量声明时的shape必须和后面约束完全对齐。我遇到过很多次“operators with mismatched shapes”的报错,排查下来基本都是某个约束拿到的向量长度不一致,尤其在使用预测序列切片时最容易犯。
2.4 参数怎么定:基于常见实践的补充说明
好几个朋友问过:Δ t取多少?N取多少?这里给一份我用过的基准配置,具体项目可以按场景调整:
| 参数 | 常用取值 | 说明 |
|---|---|---|
| Δt | 15分钟或1小时 | 15分钟更贴近日内修正,1小时适合研究验证 |
| 预测时域N | 24到96步 | 对应未来6到24小时,兼顾计算速度与视野 |
| 控制时域 | 通常等于预测时域 | 简化处理 |
| SOC范围 | 10%到90% | 给电池留保护边界 |
| 效率ηc/ηd | 0.95左右 | 锂电池典型值,具体按电芯标定 |
做研究验证时,我倾向使用1小时步长加N=24,这样算得快,曲线也清晰。做工程演示时用15分钟步长加N=96,但需要确认求解器在毫秒级能解出结果。SOC初值不要随意设置,标准做法是把仿真起点SOC固定在一个确定值,比如40%或者50%。如果仿真持续多天,建议在每天结束时把SOC通过末端约束拉回目标区间,否则长期运行会产生积累偏差。
3. Python代码实现全流程
3.1 环境准备与工具选型
先把环境说清楚。我用的是Python 3.10加numpy、pandas、matplotlib、cvxpy。cvxpy是凸优化建模库,语法接近数学表达式,特别适合MPC这类问题。求解器首选OSQP,它是专门解二次规划的,速度快且稳定。如果目标里有绝对值或其他非光滑项,可以换ECOS。
pip install numpy pandas matplotlib cvxpy如果下载速度不理想,可以配置一个更快的pip镜像源,装好后在Python里import cvxpy,跑一个最小示例确认求解器能调用。我第一次装cvxpy时遇到过求解器组件缺失,重新安装一次就解决了。
多说一句工具选型。有些朋友用casadi或者gurobi,也完全可以。casadi的符号表达式和自动微分更适合非线性MPC;gurobi在整数变量较多的场景更方便,但需要商业许可。如果只是做线性或二次型MPC,cvxpy加OSQP是最轻的一条路径,这也是我最终稳定下来的组合。
3.2 数据准备:负荷、光伏、电价序列
真实项目里,负荷和光伏数据来自历史或预测系统。研究阶段我用模拟数据来验证算法,为了接近实际情况,会叠加确定性分量和随机噪声。光伏用“早上升傍晚降”的钟形曲线,负荷用早晚双峰曲线,电价用分时阶梯。
import numpy as np np.random.seed(42) H = 24 t = np.arange(H) # 光伏:8点到17点呈现钟形 pv_base = 20 * np.exp(-((t - 12.5) ** 2) / 8) pv = pv_base + np.random.normal(0, 0.5, H) pv = np.clip(pv, 0, None) # 负荷:早晚两个高峰 load_base = 15 + 5 * np.exp(-((t - 8) ** 2) / 4) + 6 * np.exp(-((t - 19) ** 2) / 4) load = load_base + np.random.normal(0, 0.8, H) load = np.clip(load, 3, None) # 分时电价 price = np.where((t >= 8) & (t < 20), 0.8, np.where((t >= 21) | (t < 7), 0.2, 0.4))预测序列的获取很关键。如果直接拿真值加噪声当预测值,相当于假设我们有完美的持续预测模型,结果偏向乐观。如果想让实验更接近工程实际,可以用上一周期历史数据做一个简单AR模型,预测误差更大但也更真实。我把两种都跑过,最后汇报用的是AR预测版本,因为不回避误差的结论更让人信服。
3.3 MPC滚动优化核心循环
MPC的精髓就是“滚动优化”:每个控制周期开始时,拿最新的SOC和最新的预测序列,重新解一次优化问题,只执行当前这一步的控制指令,然后进入下一个周期。
下面是一个精简但能跑通核心逻辑的cvxpy建模骨架:
import cvxpy as cp def solve_mpc(soc0, pv_pred, load_pred, price_buy, price_sell, params): N = len(pv_pred) P_buy = cp.Variable(N) P_sell = cp.Variable(N) Pc = cp.Variable(N) Pd = cp.Variable(N) soc = cp.Variable(N + 1) constraints = [soc[0] == soc0] for k in range(N): # 功率平衡:负荷+充电 = 光伏+放电+购电-售电 constraints.append(load_pred[k] + Pc[k] == pv_pred[k] + Pd[k] + P_buy[k] - P_sell[k]) constraints.append(P_buy[k] >= 0) constraints.append(P_buy[k] <= params['buy_max']) constraints.append(P_sell[k] >= 0) constraints.append(P_sell[k] <= params['sell_max']) constraints.append(Pc[k] >= 0) constraints.append(Pc[k] <= params['Pc_max']) constraints.append(Pd[k] >= 0) constraints.append(Pd[k] <= params['Pd_max']) constraints.append(soc[k + 1] == soc[k] - (Pc[k] * 0.95 - Pd[k] / 0.95) * 0.25 / params['E_cap']) constraints.append(soc[k] >= 0.1) constraints.append(soc[k] <= 0.9) # 目标:购电成本 - 售电收益 + 老化惩罚 + 购电波动惩罚 objective = cp.Minimize( cp.sum(price_buy * P_buy - price_sell * P_sell + 0.01 * (Pc + Pd) + 0.05 * cp.sum_squares(P_buy[1:] - P_buy[:-1])) ) prob = cp.Problem(objective, constraints) prob.solve(solver=cp.OSQP) if prob.status != 'optimal': return None return P_buy.value, P_sell.value, Pc.value, Pd.value, soc.value这里必须说明一点,代码里没有显式写“不能同时买电又卖电”的整数互斥约束。在实际数据中,售电价低于购电价,同时买卖除了增加成本没有好处,所以目标函数会自然抑制。如果研究场景里有售电价格高于购电价格的时段,必须加二进制变量做严格互斥,否则会出现套利性质的病态解。
滚动主循环也很直接:
soc = 0.4 history = [] for k in range(144): # 6天仿真,每小时一步 pv_pred = pv_data[k:k + 24] load_pred = load_data[k:k + 24] price_buy = price_data[k:k + 24] res = solve_mpc(soc, pv_pred, load_pred, price_buy, price_buy * 0.9, params) if res is None: break P_buy_k, P_sell_k, Pc_k, Pd_k, soc_plan = res # 只执行第一步 soc = soc_plan[1] # 真实工程中应从BMS读取实测值覆盖 history.append((P_buy_k[0], P_sell_k[0], Pc_k[0], Pd_k[0], soc))这里为了演示直接取soc_plan[1]作为下一时刻状态,这在纯仿真中是常见做法。但真实现场一定要在下一周期开始时用电池BMS的实测SOC覆盖模型计算值。这一步反馈就是MPC能长时间稳定工作的根基。
3.4 评估与可视化
优化跑完之后该看什么?至少三件事:总成本降了多少、SOC是否越限、功率曲线是否平滑。我习惯画一张2×2大图,四个子图分别是功率曲线对比、SOC变化曲线、光伏与负荷曲线、累计成本曲线。
import matplotlib.pyplot as plt fig, ax = plt.subplots(2, 2, figsize=(12, 8)) ax[0, 0].plot([x[0] for x in history], label='P_buy') ax[0, 0].plot([x[1] for x in history], label='P_sell') ax[0, 0].plot([x[2] for x in history], label='P_charge') ax[0, 0].plot([x[3] for x in history], label='P_discharge') ax[0, 0].legend() ...评估指标建议用三个:系统总成本、SOC越限率(越限步数占总步数比例)、电网交互功率波动均方差。第一个看经济性,第二个看安全性,第三个看设备友好性。只看成本会漏掉很多问题,比如SOC一直贴着边界走,短期成本低,长期电池寿命却受影响。
4. 实操中的常见问题与排查技巧
4.1 求解失败、DCP报错与不可行问题
cvxpy最常见的报错之一是“Problem does not follow DCP rules”。出现这个报错基本上是目标函数或约束里有非凸表达式,比如对变量直接取绝对值。解决办法是重构模型,把max、min、abs这类操作替换为线性等价形式,典型的做法是引入辅助变量t,让t大于等于x也大于等于-x,然后在目标函数里最小化t之和。
另一个典型问题是“problem infeasible”,意思是约束过紧找不到可行解。我建议按下面顺序排查:先把互斥类约束去掉,看能不能解;如果不能,接着检查SOC更新方程的符号,很容易把充放电代数和符号写反;最后检查所有变量上下界是否冲突。大部分infeasible其实不是真的无解,而是某个变量被写死了,例如SOC下界0.9、上界0.1这种笔误,或者预测数据里出现了极端异常值。
4.2 SOC漂移与反馈校正
MPC的滚动优化如果不加入实测值反馈,长时间运行会出现SOC漂移,即仿真N步后,SOC和真实物理系统越走越远。原因不复杂,每次都用模型状态往下推,模型误差和扰动会不断累积。我第一次写代码就省掉了反馈,连续跑了5天仿真,最后SOC计算值超过1.2,怎么看都不合理,排查很久才发现是状态方程推导本身没问题,但因为模型每步都有近似,误差被一步步放大了。
解决办法其实就是强调过的那个动作:每个控制周期开始时,用实测SOC覆盖模型状态。如果做纯仿真,可以用一个带噪声的真实模型来模拟BMS反馈,比如真实SOC等于名义计算值加一个小幅噪声,再作为下一步优化初值。加上这个小改动后,SOC轨迹会平滑很多。
4.3 参数敏感性:哪些参数最值得调
我按影响程度把调参顺序排了个序,供参考:
- SOC上下限。这是硬边界,直接决定电池可用空间,影响最大。
- 优化时域N。N太短会变得短视,例如为了眼前便宜拼命放电,不考虑后面高价时段的需求。N太长则计算负担变大,而且远期预测质量差,反而拖累性能。
- 功率波动惩罚权重。权重太小,功率曲线呈锯齿状;权重太大,储能变成“按兵不动”,经济性下降。
- 效率参数。效率写错会导致SOC更新偏快或偏慢,建议用标定值而不是拍脑袋估计。
调试方法上有一个原则值得记住:固定其他参数,每改一个参数就跑三组典型场景,比如晴天、阴天、负荷高峰日,对比成本和SOC越限率再做决定。如果一次改两三个参数,最后很难判断是哪个参数起了作用。
4.4 与传统规则策略的实测对比
空谈无用,拿数字说话。我用那套AR预测版MPC和一个常见的“光伏优先、峰谷套利”规则策略做了对比。规则策略逻辑大致是:电价低谷且SOC低时充电,光伏大发且SOC低时充电,电价高峰时放电。在同样的模拟数据下,MPC在6天仿真里总成本比规则策略低大约11%,主要利润来自低谷充电、高峰放电的时机把握更准。同时MPC的功率曲线更平稳,SOC全程保持在10%到90%区间内,而规则策略在光伏剧烈波动那天几乎触碰SOC下限。
但我要给一个提醒:这个11%来自特定数据集和特定规则基准。如果拿MPC跟一个写得很粗糙的规则策略比,收益会被夸大;跟一个精心调参的规则策略比,收益可能只有5%。做对比实验时,一定要把基准策略的逻辑写清楚、场景覆盖完整,否则论文或者项目汇报里都会显得自说自话,缺少说服力。
我这个课题做下来最大的体会是:MPC真正难的地方不在算法本身,而在建模的严谨性和参数的耐心。把状态方程写对、把互斥约束想清楚、把反馈链路打通,后面基本水到渠成。如果你想用Python复现这个方向,我建议不要一上来就搞分布式MPC或者随机MPC,先跑通单储能、单微网这个版本,把滚动闭环和调试套路摸熟,再逐步叠加复杂度。这样至少不会被一连串报错打击掉信心。