有次在某园区微网项目现场,运行负责人盯着能量管理系统的曲线问我:“光伏中午出力冲得高,负荷侧又集中在早晚两个高峰,储能到底什么时候充、什么时候放,才能实打实把电费省下来?”
这个问题看上去不复杂,但一旦把分时电价、电池SOC约束、未来24小时光伏和负荷预测都摆上台面,人脑就算不过来了。每个时段的充放电决策都会影响后续所有时段,24个时段就是24个相互耦合的决策点,更别提还要应对预测偏差带来的滚动修正。这正是微网经济运行问题的核心:在满足供电可靠性和设备物理约束的前提下,用最低的总成本完成电力供给。而“用代码解析”这套复杂系统,就是把运行规则翻译成数学模型,再交给求解器做全局最优决策。
这篇文章是我从零落地一整套微网经济调度模型的完整记录,内容包括优化问题的建模过程、为什么必须用代码而不是手工表格、一套可以直接修改使用的Python求解骨架,以及我在真实项目调试中踩过的坑。适合正在做微网能量管理的工程师、想往能源方向转型的程序员,以及需要评估调度系统方案的项目管理者。
1. 微网经济运行到底在优化什么:先给复杂系统画个边界
1.1 三个基本构成单元:电源、储能、负荷
微网可以理解为一个能够自治运行的小型电力系统。典型构成有三块:分布式电源、储能和负荷。
分布式电源包括光伏、风电这类不可控电源,也包括微燃机、柴油发电机这类可调度电源。不可控电源的出力取决于天气,预测误差是经济运行里最大的扰动源;可调度电源虽然能随时调整出力,但要烧燃料、算成本,所以它的存在会让优化问题多出一组连续调节变量。
储能是微网里最特殊的环节。它能在时间维度上“搬运”电能:电价低的时候充电,电价高的时候放电,本质上是拿容量换时间的套利。但它不是免费工具,每一次深度充放都会折损寿命,所以模型必须把电池的“使用代价”写进目标函数,否则求解器会让你天天满充满放。
负荷则是必须服务的对象。刚性负荷无论电价多高都要满足,柔性负荷可以被平移或削减,参与需求响应。负荷的建模精细度,决定了优化结果离实际运行有多远。
三者接在同一条母线上,构成一个功率平衡闭环。经济调度的本质,就是在这条母线上决定每一个时段各单元的出力方向和大小。
1.2 “经济运行”的目标函数到底算哪些钱
把经济运行写成一个最优化问题,第一步是定义“成本是什么”。绝大部分微网项目的最小化目标包含以下几项:
| 成本/收益项 | 对应变量 | 说明 |
|---|---|---|
| 网购电成本 | 购电功率 × 分时电价 | 大多数并网微网最大的一项成本 |
| 售电收益 | 上网功率 × 上网电价 | 存在余电上网机制时作为负成本 |
| 储能退化成本 | 充放电循环折算 | 防止模型滥用电池的关键项 |
| 弃光弃风惩罚 | 削减的可再生出力 | 引导模型优先消纳清洁能源 |
这里有一个非常容易被忽略的细节:如果目标函数里只写购电成本,储能就会变得“近乎免费”。只要峰谷价差存在,求解器一定会让电池谷时充满、峰时放空,甚至每天一次满充满放循环。但从设备角度看,这种调度策略会让电池在两三年内衰减到没法用,实际成本远远高于省下的电费。
所以我在目标函数里加了储能使用惩罚项,通常按等效循环次数折算成每千瓦时的成本。这个惩罚不需要很大,但必须存在,它代表的是“电池不是白用的”。后面代码里你会看到我用了一个很小的系数来达到这个效果。
1.3 调度的三种时间尺度:日前、日内、实时
微网经济运行不是一次性算完拉倒,而是分层滚动决策的过程。我习惯把它拆成三个时间尺度:
日前调度用未来24小时的预测数据,生成明天各时段的运行计划,粒度通常是1小时或15分钟。它解决的是“大方向”问题,比如储能大致在哪个时段充、充多少,购电曲线什么形状。
日内滚动调度每隔一个较短周期(比如15分钟)重新求解一次,利用最新实测数据和短期预测修正日前计划。之所以必须滚动,是因为预测永远有误差——光伏实际出力可能忽高忽低,负荷可能突然冲高,如果死守日前计划,系统不是在浪费电价机会就是在用电高峰期冒险。
实时层则针对秒级波动作功率跟踪,通常由就地控制器完成。优化代码输出的是一条功率序列,落到执行层还要考虑设备响应速度和模式切换逻辑。这部分我在第5章会展开讲。
理解这三个尺度,你才知道自己写的模型应该放在哪个位置——是离线做方案比选,还是嵌入到能量管理系统的调度模块里。
2. 为什么说“用代码解析”不是锦上添花,而是绕不开的路
2.1 先感受一下问题的真实规模
很多人第一次接触微网调度时,第一反应是“拿Excel拉一张表,手动填几个充放电规则”。我理解这种直觉,因为在小规模场景下,几条经验规则确实能起到一定作用,比如“谷充峰放”“光伏大发时优先自用”。
但问题规模稍微一放大,手工就崩了。一个最简单单微网案例:24个时段、一组光伏、一组电池、一条购电通道。决策变量就包括24个充电功率、24个放电功率、24个购电功率,也就是72个变量;约束包括功率平衡24条、SOC递推24条、功率上下限若干条。这个规模对求解器来说是小菜一碟,但真正让手算无解的是耦合性——第1时段的充电决策会改变第2时段的SOC起点,第2时段又影响第3时段……到了第10时段,所谓“合理的充放电规则”已经跟直觉脱节了。
如果你做的是15分钟粒度的日内滚动,一天要解96次模型。这种情况用人工试算,无论怎么想都是不现实的。
2.2 代码带来的三个必然优势
第一个优势是可复现。同样的预测数据、设备参数,一键重跑,每次结果完全一致。这意味着你可以回头比较“上个月那套方案”和“新方案”到底差多少,这是做策略迭代的基础。
第二个优势是可对接。预测服务、SCADA采集的数据流,可以直接接到脚本里,形成自动化的“预测—求解—下发”闭环。手工表格做不到这一点,因为数据更新频率和运算速度都跟不上。
第三个优势是可扩展。同一个建模框架,今天只算单微网,明天加一台柴发、加一组需求响应、甚至扩展到多微网协同,只需要在模型里增加变量和约束,而不是推倒重来。我接手过的项目里,几乎所有系统都从简单模型逐渐长成了复杂模型,没有代码基础,这个“生长过程”根本走不动。
2.3 工具选型:Python生态为什么是首选
做微网优化,业界常见的方案有这么几类:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Python + cvxpy/pulp/OR-Tools | 开源免费、上手快、生态完整、便于接入预测模型 | 大规模MILP性能不如商业求解器 | 前期验证、技术研究、中型项目 |
| GAMS / AMPL | 建模语言规范,支持大规模问题 | 授权贵、学习曲线陡、部署不便 | 学术研究、超大规模规划 |
| MATLAB优化工具箱 | 电力行业熟悉度高、绘图调试方便 | 闭源收费、部署灵活性差 | 科研原型 |
| EMS厂商自带调度算法 | 与生产系统贴合 | 黑盒、迁移难、可解释性差 | 项目验收后直接运维 |
我的建议非常明确:团队里只要有人熟悉Python,就先用Python搭模型,用开源求解器跑通。实测下来,CBC、HiGHS、CLARABEL这些开源求解器处理24时段甚至96时段的线性规划,基本都在毫秒到秒级,完全够用。真到了求解时间或稳定性成为瓶颈的阶段,再换成商业求解器接口也不迟,模型本身不需要大改。
3. 物理规则翻译成数学约束:建模这一步决定模型质量
3.1 功率平衡约束:微网物理定律的代码表达
任何微网模型的地基,都是母线功率平衡,说白了就是基尔霍夫电流定律在微网里的体现。对单母线并网微网,每个时段所有流出功率必须等于流入功率,写成:
P_pv[t] + P_wind[t] + P_buy[t] + P_dis[t] = P_load[t] + P_ch[t] + P_sell[t]
如果微网孤岛运行,P_buy和P_sell直接置零,功率平衡完全靠内部电源和储能支撑。这一步看起来简单,但方向搞反是新手最常见的错误。我在一个早期项目里就吃过这个亏:把购电功率写到了负荷侧,模型求解出来“购电越多成本越低”,因为变量符号反了。
3.2 SOC递推约束:储能“记忆效应”的数学表达
储能的核心约束是SOC递推,它表达了电池当前荷电状态对历史的依赖:
SOC[t+1] = SOC[t] + (η_ch × P_ch[t] - P_dis[t] / η_dis) × Δt / E_bat
这里面有两个效率:充电效率η_ch和放电效率η_dis。两个效率分开写是有讲究的——储能变流器在充放电两个方向上的损耗并不完全相同,分开建模更贴近实际。效率参数的位置也不能放错,如果放反了,模型会得出“充电反而增加电量”或者“放电反而减少电量”的悖论。
Δt是时段长度,E_bat是电池额定容量。注意量纲统一:如果功率用kW、容量用kWh、时间用小时,那么Δt就是1(代表1小时),这要求你的预测数据粒度也要对应。
3.3 线性化处理:为什么要先做LP/MILP
微网调度问题天然包含不少非线性因素,比如电池充放电效率随电流变化、设备启停逻辑等。但在工程实践里,我强烈建议先把问题线性化成LP或MILP再求解。
原因很简单:线性规划求解器非常成熟,能找到全局最优解,而且求解速度快、稳定性好。一旦引入非线性,全局最优解就无法保证了,项目调试难度直接翻倍。
最典型的是充放电互斥问题。如果允许P_ch和P_dis同时为正,可能出现“边充边放”的无效循环——说白了就是同一度电在母线上转一圈,白白损耗效率。严格的处理方式是引入二进制变量y[t]:
P_ch[t] ≤ M × y[t]
P_dis[t] ≤ M × (1 - y[t])
这样模型就变成了MILP。但工程上我通常更喜欢另一种做法:在目标函数里给P_ch[t] + P_dis[t]加一个很小的惩罚系数。这么做的好处是保持LP的求解速度,代价是“理论上不严格”。对绝大多数调度场景来说,这个不严格完全不影响使用,因为物理上的控制器本来就不会允许设备同时充放。
3.4 完整模型骨架:一段可以直接改的Python代码
下面是模型的完整骨架,用cvxpy描述。注释里写清了每个变量的物理含义:
import cvxpy as cp import numpy as np T = 24 dt = 1 # 时段长度,单位小时 # 输入序列:预测值(kW)和电价(元/kWh) pv = np.array([...]) # 光伏预测 load = np.array([...]) # 负荷预测 price = np.array([...]) # 分时电价 # 储能参数 E = 200.0 # 电池容量,kWh soc_0 = 0.5 # 初始SOC soc_min, soc_max = 0.1, 0.9 eta_ch, eta_dis = 0.95, 0.95 p_max = 50.0 # 最大充放电功率,kW # 决策变量 p_ch = cp.Variable(T) # 充电功率 p_dis = cp.Variable(T) # 放电功率 p_buy = cp.Variable(T) # 购电功率 soc = cp.Variable(T + 1) # SOC,多一个初值点 # 目标函数:购电成本 + 电池使用惩罚 obj = cp.sum(price * p_buy * dt) + 0.01 * cp.sum(p_ch + p_dis) constraints = [ pv + p_buy + p_dis == load + p_ch, # 功率平衡 soc[1:] == soc[:-1] + (eta_ch * p_ch - p_dis / eta_dis) * dt / E, soc[0] == soc_0, # 初始SOC固定 soc >= soc_min, soc <= soc_max, p_ch >= 0, p_ch <= p_max, p_dis >= 0, p_dis <= p_max, p_buy >= 0, ] prob = cp.Problem(cp.Minimize(obj), constraints) prob.solve(solver=cp.CLARABEL) # 输出结果 print(prob.status, prob.value) print(p_ch.value, p_dis.value, soc.value)注意,代码里的数组符号简化了输入,实际项目里要用预测服务的真实数据填充。目标函数里的0.01是储能惩罚系数,单位是元/kWh,它防止求解器滥用电池;如果你的电池寿命数据更精确,可以按等效循环成本精算这个系数。
3.5 代码里最容易搞错的一点:SOC索引
很多人在这个位置翻车。我在代码里把SOC定义成长度为T+1的变量,初始SOC给到soc[0],递推式约束的左边是soc[1:]、右边是soc[:-1]。这样写的好处是语义清晰:p_ch[t]影响的是soc[t]到soc[t+1]的转移,不是soc[t]本身。
如果直接把SOC定义成T个变量,再看递推式,就必须处理SOC[0]是“初始值还是第一个时段终点”的问题。这种模糊性是bug温床,我建议宁可多用一个变量位,也要把语义写清楚。
4. 从预测到指令:把求解结果变成一套能跑的调度闭环
4.1 预测数据接入:模型之外的第一道质量关
再好的优化模型,输入是垃圾,输出也是垃圾。光伏预测、负荷预测、电价预测是微网经济调度的三大输入。实际项目中,光伏预测通常结合数值天气预报与历史功率数据,负荷预测则依赖历史用电曲线和星期特征,电价预测在电力市场环境下可能来自市场出清结果或短期价格预报。
接入代码时最需要关注的是时间轴对齐。我遇到过不止一次项目,因为时区、时间戳、采样粒度的差异,模型拿到的预测序列跟实际运行时刻错了好几个点,结果自然是全套计划偏差。建议在数据进模型前做一次统一转换:把全部序列对齐到同一个基准时间轴,并做单位检查。
4.2 滚动优化的主循环:预测、求解、下发、再预测
代码层面,一套可落地的调度闭环就是在一个循环里反复执行如下逻辑:
while True: pv_pred = forecast_pv() # 拉取最新光伏预测 load_pred = forecast_load() price_pred = forecast_price() plan = solve_schedule(pv_pred, load_pred, price_pred) dispatch_to_devices(plan[0]) # 下发当前时段指令 wait(next_interval)这里最关键的是“只执行计划的第一步”。因为预测会随实测刷新,滚动优化必须不断用新信息修正。你不需要信任明天的整条曲线,只需要相信未来一个时段的决策,后续每一步都有新的优化机会。这种“走一步算一步”的策略,比一次性下发全天指令稳健得多。
4.3 求解结果验证:先看可行性,再看合理性
跑完模型不能只看status,我有一套固定的验证顺序。
先做可行性检查。求解器返回optimal才算通过,如果返回infeasible,按优先级排查:SOC初值是否超出边界、负荷预测是否大于可用电源最大出力、储能容量参数是否过小。这些都是导致不可行的常见原因。
再做合理性检查。目标函数值是否处于合理量级?SOC曲线是否符合“谷充峰放”的直觉?如果SOC在半夜疯狂波动,或者购电功率出现莫名其妙的高频抖动,大概率是约束或参数出了问题。
这里分享一个我一直在用的反向验证技巧:把所有时段电价设为同一个值,然后重新求解。理想情况下,储能不能从价差中获利,SOC应该几乎保持不动,负荷直接由光伏和购电满足。如果电价一致时模型还在拼命充放,那一定是目标函数或者约束写错了。这个小测试每次都能帮我快速定位模型里的低级错误,强烈推荐。
注意:反向验证里,如果你把全天电价都设成同一个值,SOC曲线却仍然剧烈波动,那基本可以断定模型的目标函数或约束里有隐藏问题。养成这个习惯,能省不少调试时间。
5. 实战调试中踩过的四个坑:都是真实记录
5.1 时刻对齐错位:目标函数很漂亮,结果却是错的
有次我在项目里调试一套日前调度,输出的目标函数值相当漂亮,成本比原来的人工策略低了一大截,负责人很高兴。但我习惯性地把SOC曲线和充放电功率曲线叠在同一张图上看了一眼,发现问题了:SOC在下午两点就开始下降,而放电功率曲线显示下午四点才开始放。
逐项检查发现,SOC数组和功率数组错了一位。充电功率P_ch[t]影响的应该是SOC[t]到SOC[t+1]的转移,但我在初始化时让SOC[0]直接对应了第一个时段的充电,导致整个计划往后偏移了一小时。这种错误在数值上很难暴露,因为目标函数值依然合理,只有图形化对比才能看出来。
排查方法很简单:取前三个时段,手动算一遍SOC的变化,跟代码输出比对,逐项写出来,马上就能看出错位。
5.2 效率参数放错位置
效率参数这个坑我印象特别深。某次模型拿回来,充电效率η_ch写成了1.05,远超理论极限1。原因是测试时把效率单位搞混了,用“能效百分比”直接当成“无量纲系数”代入,而没意识到1.05意味着“充进去1度电能取出1.05度”,这直接违背能量守恒。
求解器并不会抗议这种荒谬参数,它只会顺着你给的成本结构找到最优解,而那个最优解可能在物理上根本不可行。所以我在建模时养成一个习惯:所有效率参数必须满足0到1之间的数值校验,并且每个效率的放置位置都要在注释里写清楚“这是充电方向还是放电方向”。
5.3 惩罚系数的量级玄学
目标函数里的储能惩罚系数,看起来很不起眼,但量级错了会让整个调度策略走形。
如果惩罚系数太小,它压制不住“边充边放”的虚耗解,更压不住储能的滥用;如果系数太大,求解器又会“不敢”用储能,宁愿在高峰时段高价购电,也不让电池放电,白白浪费了移峰填谷的价差空间。
我的一般做法是:把惩罚系数折算成每千瓦时循环成本。比如某电池按寿命曲线估算,一次满充满放等效成本约0.02元/kWh,那我就用它作为系数,再和峰谷价差比较,确保至少差一个数量级。调试时可以扫描几个倍率,观察SOC利用率和目标函数变化,找到一个“既不滥用也不浪费”的区间。
5.4 最优结果不等于可执行结果
求解器给出来的是一条“理想功率序列”,但现场设备不一定执行得了。最典型的差距有三个:一是机组爬坡速率有限,模型没建模的话,指令可能要求功率在1分钟内从0跳到满负荷;二是通信和指令刷新周期不同步,求解器算完了一批指令,下发时现场已经变了;三是预测毛刺会反映在指令上,产生不必要的功率抖动。
应对思路也很直接:在模型里补充机组调节速率约束,或者对输出指令做限幅和平滑;滚动优化时,限制相邻两次下发的指令差值,避免大起大落。这些处理虽然会让“理论最优值”略涨一点,但换来的是一条现场真的能执行的曲线,这才是工程意义上的最优。
我个人每次完成一版微网经济调度模型,都会先跑那个“电价一致”的反向测试,再把SOC曲线和功率曲线叠在一起检查波形。这两步看起来笨,却能挡住大多数模型层面的低级错误。调度的价值不在于算出一个漂亮的数学最优值,而在于那条曲线能被现场执行,并在一个月后回头看时,电费账单真的降下来了。希望这篇记录对正在做或准备做微网经济运行优化的人有点用处。