☰
基于Python的风光储能微电网日前经济调度建模与实现
2026/10/20 8:01:23 网站建设 项目流程

我大概是去年年中接了一个微电网试点项目的经济调度优化改造任务。项目本身不大:一台风机、一组光伏阵列、一套电化学储能,外加一部分可中断负荷,规模属于典型的园区级微电网。但我接手之前,调度策略一直是用人工经验表在跑,所谓“尽量多用新能源”,结果月度电费账单出来之后,财务那边脸色并不好看。原因也很直接:没有考虑分时电价下的购电成本、没有把储能的充电窗口用满、需求响应更是仅仅停留在“开会通知”的层面上。

后来我把这套逻辑完整重写成Python代码,做成基于风光储能和需求响应的微电网日前经济调度模型,一天滚动一次,求解未来24小时的机组出力计划、储能充放电计划、需求响应削减计划。效果立竿见影:典型日运行成本下降了大约14%,光伏弃电率接近清零,储能循环次数也控制在合理范围。整个过程踩了不少坑,今天把建模思路、代码架构、求解选型和排错经验都整理出来,希望能给正在做微电网优化调度或新能源方向课题的同学一些参考。

这套内容适合谁?简单说:电气工程、能源经济方向的研究生,做综合能源系统仿真的工程师,以及刚接触Pyomo/Pulp优化建模的Python使用者。我不打算堆公式吓人,但如果涉及关键约束,我会给出明确的数学表达和对应的代码实现片段,便于你照着复现。

1. 为什么微电网需要“日前调度”:买菜的比喻和24小时决策窗口

很多初学者对“日前调度”这个概念容易混淆,以为就是提前一天把所有计划定死。其实不是。它的本质是在电价、负荷预测、新能源出力预测已知的前提下,把明天24小时分成若干时段(通常是1小时一个点),求解一组最优决策变量,包括各时段向大电网购电的功率、储能充放电功率、柔性负荷的削减量和恢复量等。

用买菜来理解:我不会在早上起床之后站在菜市场门口拍脑袋,而是昨天晚上看天气预报、看冰箱剩余、看第二天家里来几个人吃饭,把第二天的菜单和采购清单事先定好。微电网调度也是一样——光伏和风电的出力曲线可以通过数值天气预报(NWP)提前预测,虽然不准,但趋势大概率可用;分时电价是电网公司提前公布的,这是硬信息;负荷侧的基线预测也可以基于历史数据做回归。综合这些信息,提前求解“明天怎么运行”的最优策略,就是日前经济调度。

为什么强调“日前”?因为很多控制是慢变量,没法实时反应。比如储能电池,它的充放电计划如果用来响应实时电价波动,往往来不及——电化学储能虽然有毫秒级响应能力,但频繁改变功率方向会显著影响循环寿命和热管理,而且大规模充放电切换对PCS(储能变流器)也是考验。又比如需求响应,你需要提前几个小时通知用户“明天下午3点到5点会削减空调负荷”,总不能说断就断。所以,一个滚动的日前计划和日内修正框架,是微电网经济运行的骨架。

这里有一个反直觉的点:日前调度不是为了“明天准确执行”,而是为了让决策有依据。因为新能源预测误差存在,日内肯定要重新调整,但如果没有前一天的基准计划,日内修正就成了无锚点的乱调,经济性和安全性都会失控。

2. 数学模型怎么搭:目标函数和约束条件逐项拆解

做经济调度绕不开优化模型。看起来是一堆线性约束的堆砌,但每个系数怎么取、边界怎么设,都直接影响解的质量。我先把目标函数和约束条件讲清楚,再给代码实现。

2.1 目标函数:成本最小化还是利润最大化?

第一步是明确优化目标。多数微电网与大电网之间有电能交互,因此有两种等价建模方式:一是最小化“微电网运行总成本”,二是最大化“微电网净收益”。两者在数学上差一个常数项,结果等价。我习惯用成本最小化表达,因为它更贴近工程人员的直觉。

目标函数包含四部分:

  1. 向大电网购电成本:每个时段从电网买电的费用,分时电价越高,这个权重越大。
  2. 储能运行损耗成本:充放电过程中的能量损耗折算成等效成本,也可以用固定每次循环的损耗费用来表示。
  3. 需求响应补偿成本:对可中断负荷削减的补偿单价,一般高于正常电价,但低于峰值电价,否则优化模型要么不削减,要么疯狂削减。
  4. 弃风弃光惩罚项:为了让模型“尽量利用新能源”,我对没有消纳的风光功率设置一个惩罚系数。这个系数取多少很讲究——太大,模型会拼命配储能去消纳最后一点新能源,导致储能循环成本高于新能源收益;太小,又会有大量弃电,失去“绿电优先”的意义。

表达式大致如下:

min Sum_t[ 购电电价(t) * P_buy(t) + 储能损耗单价 * (P_ch(t) + P_dis(t)) + 需求响应补偿单价 * DR_cut(t) + 弃风弃光惩罚单价 * 弃电量(t) ]

关于系数的取值,我推荐一个经验:需求响应补偿单价的设置应略低于峰时电价、高于谷时电价。为什么?以峰荷时段为例,如果补偿单价高于峰时电价,那么模型会倾向于把所有柔性负荷全部削减,导致用户舒适度严重受损,这不是运营方想要的;如果补偿单价太低,则模型宁可高价买电也不调用灵活性资源,需求响应就形同虚设。我在算例里取的典型值是:峰价1.2元/kWh,谷价0.38元/kWh,DR补偿0.9元/kWh,这样的相对关系才能引导出“峰时削减、谷时恢复(或转移)”的合理行为。

2.2 核心约束:功率平衡、储能递推、DR上下限

约束条件里,最容易出问题的是储能部分,因为它的状态量(SOC)有跨时段耦合——上一时段的剩余电量会直接影响下一时段能放出的最大功率。

功率平衡约束是最基本的:

P_wind(t) + P_pv(t) + P_dis(t) + P_buy(t) = P_load(t) - DR_cut(t) + P_ch(t) + P_sell(t)

右边是负荷需求减去柔性负荷削减量、加上储能充电功率。注意:负荷项在这里是“本地消耗”,储能充电也算负荷;储能放电和大电网购电则是供给侧。如果不把这一条写成等式,模型求解出来的功率之间会存在“虚功率”——比如储能同时充电又放电,从物理上看不可能,模型可能通过同时增大两边来“刷成本”或“刷收益”,所以一定不能漏。

储能约束要细分成五条:

SOC(t) = SOC(t-1) + eta_ch * P_ch(t) * dt - P_dis(t) * dt / eta_dis SOC_min <= SOC(t) <= SOC_max P_ch(t) <= P_ch_max * u_ch(t) P_dis(t) <= P_dis_max * u_dis(t) u_ch(t) + u_dis(t) <= 1

每条约束背后都有一个必须解释的逻辑:

  • SOC递推式:当前时段电量等于上一时段电量加充电能量(乘以充电效率)减放电能量(除以放电效率)。这里效率的装配位置很多人搞反,正确的是:充电时“存进去的要打折扣”,放电时“放出来的实际上消耗了更多存储能量”,所以放电项是除以效率。
  • SOC上下限:为了防止过充过放,锂电池一般设置在0.1到0.9之间。我知道有些算例为了最大化套利,把SOC下限定到0,短期仿真没问题,但真实系统里这是电池寿命杀手。
  • 充放电互斥约束:理想要用布尔变量 u_ch 和 u_dis 把充放电模式分开,建模成混合整数线性规划(MILP)。如果做成线性规划(LP),会允许同时充放。

需求响应约束相对简单,但有一点容易忽略——不能只限制单时段削减量,还要限制总削减量,否则模型会长时间、大力度地把柔性负荷压到很低,用户的空调、电梯、照明体验会崩盘。我一般加一个全天总削减上限,比如“全天总削减不超过总柔性负荷的15%”。

2.3 边界值与时间粒度:dt到底取多少?

日前调度最常见的粒度为1小时,即T=24。但如果你做的是储能充放电策略比较精细的场景,半小时甚至15分钟粒度会让SOC递推更平滑,只是决策变量数量翻倍或翻四倍,求解时间也随之上升。

我建议:先跑1小时粒度摸清策略方向,再加密粒度做验证。案例里1小时粒度求解通常毫秒级即可完成,加密到15分钟粒度时,MILP求解时间可能到几秒甚至几十秒,此时要检查求解器的容忍度设置。

还有一组边界很容易漏:储能初始SOC和末端SOC约束。为了保证调度方案的可持续性,日前调度通常要求末端SOC等于或接近初始SOC,否则今天的策略把电池放空,明天的计划就失去了起点。我在模型中加入了松弛处理:末端SOC不必严格等于初始值,但落在一个合理区间内,比如初始0.3,末端允许0.25~0.35。这种“软约束”比硬约束更好求解,也更贴近真实运营。

3. Python求解工具怎么选:Pulp、scipy还是Gurobi?

建好模型之后,最核心的工程问题就是:用什么求解器解这个MILP?很多初学者习惯用scipy.optimize.linprog,但它的限制非常明显:只支持线性规划,不支持离散整数变量。而我们的储能充放电互斥约束需要0-1变量,所以linprog行不通。

我做了几个备选方案的对比测试,直接说结论:

求解器/建模库变量类型支持开源免费求解速度表现适用场景
PuLP + CBCLP/MILP是中小规模足够教学、快速原型、项目演示
Pyomo + GLPKLP/MILP是中等规模偏慢学术建模、复杂约束表达
Gurobi(学术版免费/商业付费)LP/QP/MILP否同规模显著快于CBC大规模算例、工业级应用
scipy.optimize.linprog仅LP是中小规模线性最优纯线性问题,不含0-1变量

混合整数模型下,CBC在几十个0-1变量的算例里尚可,但我实际测试过一个扩展算例:把储能单元拆成多台机组、负荷分区分母线建模、加入网络潮流约束之后,0-1变量轻松破百。此时CBC的求解时间从原来的几十毫秒飙升到几十秒,求解器常用默认参数的gap收敛曲线也明显变慢。后来换了Gurobi,同规模模型大约3秒内收敛到0.1%的gap以内。如果手头没有商业求解器license,我建议先走PuLP,至少能确保模型结构没问题,再考虑换求解器。

3.1 为什么要用建模库而不是手写算法

你可能想问:不就二十多个变量吗,手动写个粒子群或者模拟退火行不行?我的观点很明确:能用解析/运筹求解器解决的问题,绝对不要用启发式算法硬怼。因为日前经济调度本质是个带约束的优化问题,约束越多,启发式算法越容易陷入可行域边界附近,解的质量不稳定。而MILP求解器有严格的最优性证明,至少会给gap值,让你知道解离最优有多远。

启发式算法适合的场景是什么?变量之间存在强非线性、离散组合爆炸、约束难以线性化,比如考虑机组启停时间的复杂爬坡约束。但那是另一个层次的问题了,做日前调度起步阶段,MILP是正确且可控的路径。

3.2 我把模型从LP升级到MILP的“翻车经历”

这里分享一个具体教训。第一版代码我为了省事,把储能充放电互斥约束删了,只靠SOC递推和效率约束想“自然而然地”避免同充同放。结果求解出来的方案里,某些时段储能一边充电一边放电,两边功率数值都不大,但成本被凭空抬高了。后来加上0-1互斥变量之后,解立刻恢复正常,总成本反而下降。

这就是典型的“约束缺失导致模型利用漏洞”,初学者非常容易踩。所以我的建议是:模型里每一个工程上不可能出现的行为,都必须有对应约束去禁止,不要指望目标函数自动给你正确行为。尤其这类带能量转换效率的场景,模型的“坏点子”比你想象的多。

4. 代码实现架构:从数据准备到结果可视化的完整流程

建模语言和能力都讲透了,现在直接进入代码层面。整个项目我拆成六个模块:数据读取、参数配置(常量)、模型建立、求解、结果提取、结果可视化。模块化不是为了炫技,而是让你后续改边界条件、换求解器或者接入新的预测数据时,不用动核心建模逻辑。

project/ ├── config.py # 所有常量参数:电价、负荷基线、设备参数 ├── data_loader.py # 读取CSV/Excel预测数据 -> DataFrame ├── model_builder.py # 构建优化模型 -> 返回模型对象 ├── solver.py # 调用求解器并检查求解状态 ├── result_handler.py # 从模型对象中提取变量值、计算关键指标 └── visualization.py # 绘制调度曲线图(matplotlib)

我实际写代码时,习惯把所有与业务逻辑相关的常量、边界条件都放在config文件里,这样以后改分时电价表快很多,不用在模型代码里找数字。以下给出每个模块的关键实现要点。

4.1 数据准备:预测数据怎么组织

先不说建模,数据准备往往是耗时最多的环节。我推荐把数据组织成整型索引,而不是用字符串时刻作为索引,因为线性规划建模中所有时段都要按下标操作,字符串索引会让你写出大量if-else分支。

数据表中至少要有这些列:

  • hour:0到23的整数
  • pv_power:光伏预测值,单位kW
  • wind_power:风电预测值,单位kW
  • load_base:负荷基线预测值,单位kW,不含柔性负荷削减量
  • price:分时电价,从电网发布的信息映射而来,单位元/kWh
  • flex_ratio:该时段柔性负荷占总负荷比例,用于设定DR削减上限

注意:预测数据最好不要直接取当天的实测值做平滑,因为日前调度的本质是“用预测做决策”,你用完美实测曲线下去优化会得到过于乐观的结果,而实际运行时表现会差一大截。建议在测试阶段加入一定的高斯扰动模拟预测误差,这样验证出来的策略才接近真实性能。

4.2 模型构建:PuLP核心建模片段

我用PuLP来写建模代码,因为它的语法足够直观,且CBC求解器随包自带,开箱即用。下面给出核心片段:

import pulp def build_model(data, config): T = len(data) m = pulp.LpProblem("Microgrid_Day_Ahead_Dispatch", pulp.LpMinimize) # 1. 决策变量 p_buy = pulp.LpVariable.dicts("P_buy", range(T), lowBound=0) p_sell = pulp.LpVariable.dicts("P_sell", range(T), lowBound=0) p_ch = pulp.LpVariable.dicts("P_ch", range(T), lowBound=0) p_dis = pulp.LpVariable.dicts("P_dis", range(T), lowBound=0) dr_cut = pulp.LpVariable.dicts("DR_cut", range(T), lowBound=0) abandon = pulp.LpVariable.dicts("Abandon", range(T), lowBound=0) # 0-1互斥变量 u_ch = pulp.LpVariable.dicts("u_ch", range(T), cat=pulp.LpBinary) u_dis = pulp.LpVariable.dicts("u_dis", range(T), cat=pulp.LpBinary) soc = pulp.LpVariable.dicts("SOC", range(T + 1), lowBound=config.soc_min, upBound=config.soc_max) # 2. 目标函数 cost_terms = [] for t in range(T): cost_terms += [ data.price[t] * p_buy[t], # 购电成本 config.cost_battery * (p_ch[t] + p_dis[t]),# 储能损耗 config.cost_dr * dr_cut[t], # 需求响应补偿 config.penalty_curtail * abandon[t], # 弃风弃光惩罚 ] m += pulp.lpSum(cost_terms) # 3. 约束 # SOC递推:soc[t+1] = soc[t] + eta_ch * p_ch[t] * dt - p_dis[t] * dt / eta_dis for t in range(T): m += soc[t + 1] == ( soc[t] + config.eta_ch * p_ch[t] * config.dt - p_dis[t] * config.dt / config.eta_dis ) # 功率平衡 m += ( data.wind_power[t] + data.pv_power[t] + p_dis[t] + p_buy[t] == data.load_base[t] - dr_cut[t] + p_ch[t] + p_sell[t] + abandon[t] ) # 充放电互斥 m += p_ch[t] <= config.p_ch_max * u_ch[t] m += p_dis[t] <= config.p_dis_max * u_dis[t] m += u_ch[t] + u_dis[t] <= 1 # 需求响应单时段上限 m += dr_cut[t] <= data.load_base[t] * data.flex_ratio[t] # 全天需求响应总量上限 m += pulp.lpSum(dr_cut[t] for t in range(T)) <= config.total_dr_cap return m

代码本身不复杂,但这里有三个细节值得停下来看一眼:

第一,弃电变量abandon[t]加在了功率平衡的“源”侧。当风光出力超出负荷和储能吸收能力时,多余电量通过这个变量被“舍弃”。目标函数里给它惩罚单价,模型会在成本压力下尽量少弃电。

第二,SOC变量索引是T+1。从SOC[0]到SOC[24]共25个点,递推关系从SOC[0]连接到SOC[24],这样目标函数和约束能自然地访问初始和末端状态。

第三,0-1变量的使用方式:p_ch <= P_ch_max * u_ch这种写法叫“大M约束”的简化版。当u_ch为0时,充电功率被强制为0;当u_ch为1时,充电功率可以取0到上限之间任意值。如果没有这个大M约束,互斥变量就形同虚设。

4.3 求解与结果提取:别只用一行print就完事

求解器调用和结果提取也有门道:

solver = pulp.PULP_CBC_CMD(msg=True, gapRel=0.001) status = model.solve(solver) if pulp.LpStatus[model.status] == "Optimal": print("最优解找到,目标函数值 =", pulp.value(model.objective)) else: print("求解状态:", pulp.LpStatus[model.status])

很多人写完求解代码,感觉输出一下目标函数值就结束了。但实际业务中你还需要从模型里把每个变量的取值提取出来,整理成Pandas DataFrame,用于绘制曲线和写报告。我的做法是写一个result_handler.extract()函数,一次性把p_buy、p_ch、p_dis、soc、dr_cut全部转成DataFrame列,然后合并原始数据。

这个过程中最容易踩的坑是:变量索引和原始数据索引不一致。比如你在模型里对hour从0到23建立变量,但提取时用了1到24的下标对齐,一错全错。我建议提取时干脆用同一个整数列表从头到尾贯穿生成,不要在中间做偏移。

5. 典型算例分析:一个含风机光伏储能DR的微电网

理论部分说完了,看一个具体的算例结果。我先声明:以下所有数据都是模拟数据,不代表任何具体地区的真实电价或负荷,只是用来演示模型的行为规律。

5.1 算例环境参数

参数数值
储能容量500 kWh
储能最大充/放电功率200 kW
充电效率0.95
放电效率0.95
初始SOC0.3
风机额定功率300 kW
光伏额定功率200 kW
日负荷峰值约650 kW
峰时电价1.2 元/kWh
谷时电价0.38 元/kWh
DR补偿单价0.9 元/kWh

分时电价时段设置:早上8点到11点、下午18点到21点为峰段;其余时间为平时段;凌晨0点到6点为谷段。柔性负荷大约占负荷峰值的20%。

5.2 调度结果的典型行为模式

求解最优方案之后,有几个非常典型的模式值得记住:

**第一个模式:谷段充电,峰段放电。**储能基本遵循“电价低谷囤电、电价高峰放能”的套利逻辑。谷段(比如凌晨2点到5点),SOC从0.3逐步充到0.85左右;峰段(比如晚上19点到21点),SOC回落到0.3附近。这个行为几乎是所有经济调度模型的共性,也很容易验证:储能不参与任何物理生产,它的唯一价值就是“时间搬运能量”。

但这里有一个细节:储能并不是每个谷段都满充满放。因为末端SOC约束要求SOC[24]接近SOC[0],模型会预留一部分电量,不会在最后一个峰段把电池彻底放空。否则今天放空了,明天凌晨充电的起点太低,会影响明天峰段的放电能力。

**第二个模式:需求响应集中在最高价时段,但不会用满上限。**算例中最高的3个小时(比如19、20、21点),DR削减量达到该时段柔性负荷上限的80%左右,但全天总削减量没有达到15%上限。这说明模型在成本驱动下找到了性价比最优的削减量:多削减一度,省下的购电成本是与补偿单价之差,边际收益为正就减,边际收益为负就不减。

**第三个模式:弃风弃光基本为零。**因为储能容量足够大,加上需求响应削峰腾出的空间,绝大多数风光出力都能被消纳。我特意做了一个敏感性测试:把储能容量降到300 kWh之后,深夜光伏高发时段开始出现明显弃电,此时模型宁可弃掉一部分风光,也不让储能过充或让DR降到舒适度底线以下。这说明在储能容量不足时,弃电是必要的经济学选择,而不是系统故障,这也是为什么目标函数里必须要有弃电惩罚项的原因。

5.3 结果可视化与敏感性分析

我强烈建议加上可视化,不是为了画图好看,而是为了快速发现模型中的异常行为。有一次我画完图发现储能在某些时段出现“阶梯状”波动,SOC曲线不是平滑的充电/放电弧线,而是一格一格跳变,后来检查发现是SOC递推时长dt误设置成1.5小时,而其他约束都用1小时,导致能量不守恒,曲线出现了不合物理的断层。

除了基本曲线,我还会做三个敏感性分析:改变峰谷电价差、改变DR补偿单价、改变储能容量,观察系统的总运行成本和光伏利用率怎么变化。这些结果能直接支撑技改决策,比如“要不要扩充储能容量”“DR补偿单价定在多少才能有效引导用户参与”。

6. 实战中不容忽视的五个细节与反思

最后聊几个日常文档很少写、但做实际项目一定会遇到的细节问题。这些细节看起来小,却能左右整个项目的可用性。

6.1 注意负荷数据的“正负号约定”

功率平衡约束里,负荷是消耗功率,购电是注入功率。如果你把负荷写成负数,购电写成正数,两条逻辑往一个等式里放,非常容易在符号上出岔子。我的建议是约定俗成:所有“生产侧”变量加号,所有“消耗侧”变量加号,把等式写成“生产总和 = 消耗总和”,一眼就能验算。检查方法也很简单:把t=10时段的所有变量值代入等式,手动计算左右两边相等,误差在10^-6以内才算通过。

6.2 求解状态一定要检查,不能只依赖目标函数值

有一次我把DR削减量的总量上限写成负数,求解器返回“Infeasible”,但我没检查求解状态,顺手就把目标函数值打印出来了。结果是Python抛异常,程序崩溃。后来我加了一个强制判断:如果求解状态不是Optimal,程序直接报错,绝不往后续流程走。这个习惯救了我很多次。

6.3 新能源出力为0时,弃电变量可能产生“数值噪音”

夜间光伏出力为0,但如果弃电变量没有限上界,模型有可能在某些时段让弃电变量取极小的正值,虽然对目标函数影响微乎其微,却会在结果里留下突兀的毛刺。解决方式是对弃电变量设置物理上界,比如:[0, 最大可用新能源功率]。这样不仅消除数值噪音,也让模型更快收敛。

6.4 日前调度与日内修正的衔接:建议加一个“再调度”逻辑

很多同学把模型跑完就认为项目结束了,但实际工程里日前计划只能作为基准。基于实测值,你还需要做日内滚动修正,比如每15分钟重新用当前SOC、当前负荷、当前可用新能源更新一次计划。

我建议在架构上预留一个real_time_update()接口,输入当前实测SOC和未来4小时预测数据,重新求解一次短时域的调度问题。这样整个系统才算闭环,否则“日前调度”永远是纸面方案。

6.5 关于求解器的License与部署限制

如果你在学校,Gurobi学术版注册很容易,求解速度远超CBC。但如果未来把项目部署到公司服务器上,学术版License就不能用,商业版价格不菲。建议在项目架构设计时,把求解器封装成独立的Solver类,后续可无痛切换到其他求解器。我在第一版代码里直接写了pulp.PULP_CBC_CMD(),后来把这个调用点抽出来放到配置文件里,切换求解器只改一行参数,完全不影响其余代码。


做个简单的收尾吧。做微电网日前经济调度这个方向,我最大的感受是:模型的复杂度不在于公式多深,而在于你有多少工程细节没有把握好。从数据清洗、参数标定到约束设计、求解结果验证,每一步都可能让优化结果失去实际意义。如果你正在写相关代码,我建议先搭一个能跑的最小模型,验证结果后再逐步加细粒度、加多时间尺度。个人实际测试中,最小可行模型从零到跑通大约只需要两天,但把它扩展成可靠的生产级代码,还需要投入大量时间做边界条件测试和异常场景演练。

代码我放在了自己的工程仓库里,结构与你上面的模块划分基本一致。后续我还计划补充考虑线路潮流约束的微电网版本,以及多微电网之间的互联互济模型,等有了实际测试数据再来更新。如果你也做过类似的经济调度,欢迎在评论区聊聊你的建模习惯——尤其是DR补偿单价怎么定、储能SOC末端约束怎么设,这两块我觉得实操空间最大,也很想看看别人的经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询