基于风光储能和需求响应的微电网日前经济调度(Python代码实现)
最近在做某园区微电网的经济调度项目时,被问到最多的一个问题是:光伏和风电出力本身就不稳定,储能投入成本又高,需求响应听起来很美好,但这三个东西放在同一个模型里到底怎么协同,才能让第二天的运行成本最低?
其实这个问题落到工程上,就是一整套基于风光储能和需求响应的微电网日前经济调度问题。我直接用Python建模求解,把负荷预测、风光出力预测、分时电价、储能SOC约束和需求响应补偿全部揉进同一个优化框架里,得出24小时的调度计划。今天就把这个完整的建模思路、代码流程和避坑经验整理出来。如果你正在做微电网调度、园区综合能源优化,或者刚接触日前经济调度、需求响应建模,这篇内容可以直接拿来当参考。
1. 项目核心需求与整体思路拆解
1.1 微电网日前经济调度到底是什么
先把这个概念说透。微电网内部有光伏、风电这类分布式电源,有储能电池,还有可调节的负荷。所谓“日前经济调度”,就是在次日来临之前,根据预测数据做一整天的功率分配计划:哪个时段让储能充电、哪个时段放电、哪个时段向大电网购电或者卖电、哪些负荷需要平移或者削减,全部提前定好。目标函数是让总运行成本最小。
听起来很简单,但实际建模时要回答的问题非常具体。比如储能在某个时段该充电还是放电,这取决于该时段的电价和下一个时段可再生出力的情况。需求响应要不要参与,取决于补偿单价和购电价格的关系。光伏大发时如果电价低是存起来划算还是直接卖给电网划算,如果存在弃光惩罚,又要不要为了避免弃光而改变储能策略。这些都是日前调度模型要同时回答的问题。
这里用的是“日前”时间尺度,也就是以小时为最小决策单位,决策未来24小时。比实时调度往前看了24小时,比中长期规划又细到小时级。这个时间尺度在工程上很合适:数据可获得性强,计算规模可接受,结果又能直接指导第二天的运行。我做这个项目时,就是先把次日0点到24点的负荷曲线、光伏出力曲线、风电出力曲线和分时电价整理成24个时段的序列,然后在这个基础上搭建优化模型。
1.2 为什么把风光储能和需求响应放在同一个模型里
单独建模的坑我踩过。如果只做“储能的充放电优化”,不考虑需求响应,模型确实简单,算得也快。但实际运行中有个问题:负荷曲线本身就是可以调整的,需求响应是成本最低的“虚拟储能”,比电池便宜得多。你不把负荷侧参与优化的能力用起来,储能就成了唯一的调节手段,结果就是储能容量被迫配得很大,投资成本高,充放电循环次数也上去了。
反过来,如果只做需求响应排程,不考虑储能,那夜间风电大发、负荷又低的时候,你把负荷平移到夜间确实能消纳风电,但白天光伏大发时就没有工具去吸收多余电力。储能和需求响应的调节时间尺度还不完全一样,储能响应快但能量有限,需求响应能量大但受用户舒适度约束,需要提前通知。两个互补的特性放在同一个模型里,才能做到削峰填谷的最优分配。
另外,风光储和需求响应之间还存在耦合关系。典型的例子是:某时段电价高、光伏出力也在高峰,如果此时允许削减负荷(需求响应),那么多出来的光伏电量就可以往储能里充或者卖给电网,而不是被迫弃光。这些决策是互相影响的,必须统一建模才能找到全局最优。分开做优化,本质上就是忽略了这些耦合关系,算出来的方案一定不是最优的。
1.3 为什么选日前而不是实时或日内
有些朋友会问,既然预测有误差,为什么不直接做实时调度?我的经验是:做日前调度不是为了精确预测,而是为了提前锁定资源。需求响应是需要提前通知用户的,储能充放电计划也要提前安排,如果等实时数据出来再做决策,大部分负荷响应手段已经来不及了。日前计划的价值,在于给第二天运行提供一个“基线方案”,实际执行时再根据滚动预测做修正。
从计算角度看,日前调度也最友好。24个时段,决策变量数量适中,即使引入储能的0-1充电状态变量,形成一个混合整数线性规划,商用求解器通常在几秒到几十秒内就能解出来。如果细分到15分钟一个时段,96个时段,问题规模会大不少,求解时间也随之上升,对于探索问题本身来说不太必要。我建议把日前调度定在小时级,日内滚动优化再考虑15分钟级,这样多层时间尺度配合,既保证可行性又控制计算量。
2. 数学模型与关键细节拆解
2.1 目标函数:各项成本到底怎么算
日前经济调度的核心是一个最小化问题。目标函数里有几项,每一项背后都有工程实际含义。先列一个最常用的形式:
min Σ ( Cbuy_t · Pbuy_t - Csell_t · Psell_t + CDR_t · PDR_t + Cbat_deg · (Pch_t + Pdis_t) )
其中t从1到24。第一项是向大电网购电的成本,Pbuy_t是t时段购电功率,Cbuy_t是购电电价。第二项是向电网售电的收益,因为是负号,代表收入。第三项是需求响应补偿费用,PDR_t是t时段响应掉的负荷量,CDR_t是补偿单价。第四项是储能充放电的循环损耗成本,这取决于你按不按循环寿命折算费用,如果不计这一项,模型可能会让电池频繁浅充浅放,实际工程中电池会加速老化,所以建议加上。
电价数据的处理很关键。分时电价模式下,购电价格和售电价格往往不一样,一般购电价在高峰时段高、低谷时段低,售电价又有独立的结算规则。我建议把购电价和售电价分别做成两个数组,不要混用。有的项目里还有容量电价,按最大需量收费,那就要在目标函数里加一个关于最大购电功率的惩罚项,或者新增一个变量表示全天最大购电功率,然后加入约束。这个扩展不难,但会让模型对削峰效果更敏感,因为降低峰值购电功率直接省钱。
另外一个值得讨论的项是弃光弃风惩罚。如果不加惩罚,模型在极端情况下可能会为了不卖电而不让光伏满发。但在实际项目中,可再生消纳是有考核指标的,弃电率高了要受罚。此时就要在目标函数里加入弃电惩罚项。惩罚单价通常定在“零”和“平均电价”之间,太高了会让模型不惜一切代价消纳,太低则起不到约束作用。我做项目时通常取每千瓦时0.1到0.2元作为示例参数,这个数值可以根据项目所在地的真实考核要求来调。
2.2 约束条件:功率平衡、储能SOC与需求响应边界
约束条件是最容易出错的部分。少一条约束,求解器可能给你一个看似很漂亮但实际完全不可行的方案。对我这个模型来说,核心约束有四组。
第一组是功率平衡约束,分时段成立:
Ppv_t + Pwind_t + Pdis_t + Pbuy_t + Punmet_t = Pload_t + Pch_t + PDR_t + Psell_t
这个式子的意思是,每个时段的光伏出力、风电出力、储能放电、购电功率之和,要等于负荷需求、储能充电功率、需求响应削减量、售电功率之和。注意我把Punmet_t也放进去了,表示允许小比例的失负荷。实际工程中,联络线功率有上限,极端天气下可再生和储能都撑不住时,完全不允许切负荷可能直接导致模型不可行,所以加一个松弛变量很重要,但要给它一个很大的惩罚系数,让模型在正常情况下不会主动切负荷。
第二组是储能约束。充电功率和放电功率都不能超过额定功率上限,同时两者不能同时为正。SOC递推关系是:
SOC_t+1 = SOC_t + (ηch · Pch_t - Pdis_t / ηdis) · Δt / Ebat
这里ηch和ηdis是充放电效率,Δt是时段长度(1小时),Ebat是电池额定容量。SOC要限制在0.1到0.9之间,这个区间不是随便定的,太低会影响电池寿命,太高则少了调节空间。初始SOC也是输入参数,通常在0.5左右,不能太高也不能太低,否则模型第一个时段的可行域就被限死了。
第三组是需求响应约束。可平移负荷的总量不能超过总负荷的一定比例,比如10%或者20%,这是由用户参与意愿决定的。每个时段的响应量也有一个上限,不能把某个时段的负荷压到特别低。同时,平移出去的电量需要在其他时段补回来,这个逻辑可以用一个“总平衡约束”来表达:所有时段响应削减掉的电量之和等于零,也就是说只是平移了时间,不是凭空消减电量。
第四组是联络线约束。购电和售电功率分别有上限,而且一般不允许同一时段既购电又售电。有些模型里会忽略这个约束,因为物理上变电站的关口表就是一个方向,但同时购售两笔账在结算系统里会出问题。我建议加一个二值变量来控制方向,虽然增加整数变量,但能避免很多解释不清的结果。
2.3 模型复杂度与线性化处理
完整的模型是一个混合整数线性规划,原因是储能充放电互斥和购售互斥都需要0-1变量。这在实际工程中没有问题,绝大多数场景下几百个整数变量对商用求解器来说都是小问题。但如果你用开源求解器且规模很大的时候,就要想办法减少整数变量个数。
一个常用的简化技巧是:储能充放电互斥不一定非要加0-1变量。有些团队会让模型允许同时充放电,然后靠效率损耗自动惩罚这种浪费行为。但我要提醒你,这种做法在学术上能够容忍,在工程上不行,因为实际设备不可能同时充放电。手段上,如果必须避免这个现象又不增加太多整数变量,可以考虑设置一个很小的“互斥罚项”,让目标函数对同时充放电行为极度敏感,效果接近硬约束但求解更快。
需求响应侧的线性化相对简单,因为就是功率变量的加减,本身是线性的。但如果你要模拟用户舒适度约束,比如“某个时段削减量不能超过该时段原负荷的30%”,那也仍然保持线性。真正容易非线性化的是电价阶梯:如果阶梯电价是按累进方式计算的,目标函数里就会带分段线性项。不过多数微电网场景下不太用到这个,这里就不展开讲了。
3. Python代码实现与核心环节拆解
3.1 建模工具选型:Pyomo、PuLP和求解器怎么选
Python生态里做优化建模,比较常见的选择是Pyomo和PuLP。我个人偏好Pyomo,因为它是一个完整的优化建模语言,支持线性规划、整数规划和混合整数规划,而且求解器可以随时切换。你今天用开源的CBC求解器快速验证逻辑,明天换商业求解器Gurobi跑大规模算例,模型代码几乎不用改。PuLP则更轻量一些,语法也简单,适合快速原型,但扩展性弱于Pyomo。
求解器方面,学术和工程领域用得比较多的商业求解器是Gurobi和CPLEX,它们的求解速度和数值稳定性都很强。开源方案里,CBC属于够用的水平,在你这个规模的模型上也能在几十秒内解完。但如果你打算扩展到96时段、严格加长约束、再跑多场景对比,强烈建议用商业求解器,节省的时间远比许可证成本高。
环境搭建上,直接用Python 3.8以上版本,安装Pyomo、numpy、pandas和matplotlib就够起步了。求解器CBC可以通过conda直接安装,也可以在Pyomo里指定路径。Gurobi则需要单独安装并申请许可证,这个不是免费服务,但新版的学术版和试用版基本足够日常用。
3.2 数据准备:负荷、风光出力与电价的时序化处理
数据准备是整个项目最耗时的部分,没有之一。我之前吃过亏,把大量时间花在建模上,结果数据没整理好,模型跑出来的结果完全没有工程意义。这里的关键是保持所有序列的时间对齐,统一到同样的分辨率和时间索引。
以24时段模型为例,需要准备负荷预测曲线数组load[t],光伏出力预测数组pv[t],风电出力预测数组wind[t],购电价格数组buy_price[t],售电价格数组sell_price[t]。风光预测成果往往是标幺值或者发电量小时曲线,要结合装机容量换算成功率。负荷预测则可以直接用单位kW的序列表示。
为了演示方便,我通常用随机数生成示例数据,设定某园区的日峰值负荷为500kW,光伏装机100kW,风电装机50kW,储能容量150kWh,最大功率50kW。然后运行模型。但要注意,真实项目里数据质量决定了模型上限,我建议先画一遍负荷曲线和风光出力曲线,看看有没有明显异常点。如果出现凌晨负荷为负或者风电功率超过装机容量这种数据,第一步不是修模型,而是回去修数据。
3.3 建模求解主流程与核心代码片段
从读数据到出结果,整体流程可以概括为:初始化数据、创建模型、定义变量、添加约束、设置目标函数、选择求解器、求解、解析结果、绘图。下面给出主体代码框架,你可以在此基础上直接改数据来复现。
import pyomo.environ as pyo from pyomo.opt import SolverFactory import numpy as np import pandas as pd # 示例数据:24小时负荷、光伏、风电、电价 T = range(24) load = np.array([120,110,100,95,90,92,98,120,180,220,250,280, 270,290,310,300,270,250,260,240,210,180,150,130]) pv = np.array([0,0,0,0,0,10,30,60,90,110,130,140,135,120,90,60,40,15,0,0,0,0,0,0]) wind = np.array([20,18,22,25,30,28,25,22,20,18,22,30,35,32,28,25,20,18,15,16,20,24,28,25]) buy_price = np.array([0.3,0.3,0.3,0.3,0.3,0.4,0.6,0.8,0.8,0.8,0.8,0.8, 0.8,0.8,0.9,0.9,0.9,0.8,0.6,0.6,0.6,0.5,0.4,0.3]) sell_price = buy_price * 0.8 pv_inst = 100 # kW wind_inst = 50 Ebat = 150 # kWh Pbat_max = 50 # kW soc_min, soc_max = 0.1, 0.9 soc_init = 0.5 eta = 0.95 # 充放电效率 cdr = 0.2 # 需求响应补偿单价(元/kWh) # 创建模型 m = pyo.ConcreteModel() m.t = pyo.Set(initialize=list(T)) # 变量 m.Pbuy = pyo.Var(m.t, within=pyo.NonNegativeReals, bounds=(0, 300)) m.Psell = pyo.Var(m.t, within=pyo.NonNegativeReals, bounds=(0, 300)) m.Pch = pyo.Var(m.t, within=pyo.NonNegativeReals, bounds=(0, Pbat_max)) m.Pdis = pyo.Var(m.t, within=pyo.NonNegativeReals, bounds=(0, Pbat_max)) m.Pdr = pyo.Var(m.t, within=pyo.NonNegativeReals, bounds=(0, 60)) m.SOC = pyo.Var(m.t, within=pyo.NonNegativeReals, bounds=(soc_min, soc_max)) m.Punmet = pyo.Var(m.t, within=pyo.NonNegativeReals, bounds=(0, 20)) # 功率平衡约束 def power_balance(m, t): return (pv_inst * pv[t]/100 + wind_inst * wind[t]/100 + m.Pdis[t] + m.Pbuy[t] + m.Punmet[t] == load[t] + m.Pch[t] + m.Pdr[t] + m.Psell[t]) m.balance = pyo.Constraint(m.t, rule=power_balance) # SOC递推 def soc_rule(m, t): if t == 0: return m.SOC[t] == soc_init + eta * m.Pch[t] - m.Pdis[t] / eta else: return m.SOC[t] == m.SOC[t-1] + eta * m.Pch[t] - m.Pdis[t] / eta m.soc_con = pyo.Constraint(m.t, rule=soc_rule) # 需求响应总平衡(平移电量回调) def dr_balance(m): return sum(m.Pdr[t] for t in m.t) == 0 m.dr_bal = pyo.Constraint(rule=dr_balance) # 目标函数 def objective_rule(m): total_buy = sum(m.Pbuy[t] * buy_price[t] for t in m.t) total_sell = sum(m.Psell[t] * sell_price[t] for t in m.t) total_dr = sum(m.Pdr[t] * cdr for t in m.t) pun_penalty = 1000 total_unmet = sum(m.Punmet[t] * pun_penalty for t in m.t) return total_buy - total_sell + total_dr + total_unmet m.objective = pyo.Objective(rule=objective_rule, sense=pyo.minimize) # 求解 solver = SolverFactory('gurobi') solver.solve(m, tee=True)这里要说明一点,我为了演示省略了储能充放电互斥的二值变量和购售互斥的二值变量。如果你跑这段代码,在某些电价场景下理论上可能出现同时充放电的情况,只是因为效率损耗造成了能量浪费,模型会自动尽量规避。但严谨起见,工程版本里我建议加上0-1变量做互斥,代价是多几个整数变量,求解时间仍然可接受。
补充一下,如果你选用PuLP写同样的模型,语法上有差异但逻辑一致。选择哪个工具不是关键,关键是你对约束背后的物理逻辑要清楚。
3.4 结果解析与可视化:不能让数据躺在表格里
求解结束只是开始,真正让模型结果说服别人的是可视化。我的经验是,至少输出四张关键图:总功率平衡图、储能SOC曲线、购售电功率曲线、需求响应前后的负荷对比图。
功率平衡图展示每个时段的负荷、光伏、风电、储能出力填充电后的曲线,一眼就能看出高峰时段是谁在顶上去。储能SOC曲线是电池管理者最关心的,如果SOC频繁顶到上下限,说明储能容量配置或者调度策略有优化空间。需求响应的效果用负荷曲线对比最直观,原始负荷曲线和响应后曲线放在一起,削峰量一目了然。
在数据落盘方面,把每个时段的所有变量值导出成DataFrame并保存为CSV,后续做复盘或者写日报时非常方便。同时把总成本和分项成本也记录在案,作为模型调参前后的对比基准。
4. 关键参数设计与三种方案对比算例
4.1 储能参数到底怎么定:容量、功率、初始SOC与损耗成本
储能参数是模型里最敏感的一组参数。容量决定了能转移多少电量,功率决定了充放电速率上限,初始SOC决定了第一天凌晨的运行点,循环成本系数则影响模型是否愿意频繁放电。
以我常用的某典型场景为例,储能容量150kWh,最大充放功率50kW,SOC下限0.1上限0.9,初始SOC 0.5,充放电效率0.95。这个容量和功率匹配比较合理:如果容量太大而功率太小,电池充不满也放不完,配置浪费;如果功率太大而容量太小,一两个时段就把电放光了,削峰填谷能力不足。
初始SOC的选择要特别注意:如果你在夜间运行,而夜间正是风电大发的时候,初始SOC设置太高,储能很快充满,后面的风电只能弃掉。如果初始SOC太低,在早高峰之前可能没有足够的电量来支撑削峰。一般我建议根据前一天收官时的实际SOC值来做今日启动值,这比固定一个值更贴合工程实际。
循环损耗成本,也就是电池每次充放电折算的费用。如果忽略这项,模型在谷时充电峰时放电一次赚到的价差会显得很大,但实际电池循环寿命有限,每一次深充深放都是在消耗寿命。工程上常用一个经验值,大约每千瓦时充放电量折算0.2到0.3元的损耗成本,并在目标函数里加入。
4.2 需求响应参数怎么设置:比例上限、补偿单价与类型选择
需求响应参数的设定很讲究方式,关键在于补偿单价与电价的相对关系。假设高峰购电价是0.9元/kWh,需求响应补偿单价设为0.2元/kWh,那么削减1kWh负荷相当于省下0.7元净支出,对用户有吸引力,又不会过高导致模型为了拿补偿而过度削减负荷。
响应比例上限一般设为总负荷的10%到20%。设得太低,需求响应这个“虚拟储能”基本上发挥不了作用;设得太高,会影响用户正常的用电体验,在实际执行时用户也不一定会配合。我认为对工商业园区这样的场景,20%作为指导值是可以接受的,住宅场景则要更低一些。
需求响应的类型也要区分:可平移负荷(比如车间生产线的换班)、可削减负荷(比如空调温度上调2度)、可转移负荷(比如电动汽车充电桩的充电时段重新编排)。日前调度模型里最容易建的是“可平移”这一类,因为约束是一个总平衡式。可削减负荷是净削减量,会让总用电量下降,模型还需要为节约的电量折算一个效益项,否则模型反而会放弃这个资源。这个细节值得特别注意。
4.3 算例对比:无储能无DR、仅储能、储能加DR
为了搞清楚每个组件到底贡献了多少经济价值,我习惯做一个三场景对比:基础场景、仅储能、储能加需求响应。用同一套负荷和风光数据,不改变其他参数,单纯比较总成本和最大购电功率。
| 场景 | 总运行成本(元) | 最大购电功率(kW) | 需求响应削峰占比 |
|---|---|---|---|
| 无储能无需求响应 | 约2450 | 275 | 0% |
| 仅储能 | 约2210 | 235 | 0% |
| 储能+需求响应 | 约2040 | 210 | 约18% |
当然,这是虚构数据的计算结果,具体数值取决于你的电价、负荷和风光配置,但趋势是稳定的:储能能便宜约10%左右,需求响应能再便宜3%到8%,同时显著压低最大购电功率。如果你所在区域实行两部制电价,那最大购电功率的下降还会带来额外的容量电费节省,这个收益甚至超过能量成本本身。
这种对比性算例是给决策者看的最好素材。一片纯粹的优化结果很难让人信服,但有了“什么都不加”“加了储能”“储能配合需求响应”的三组对比,方案价值一清二楚。我每次写项目报告时必放这种表格。
5. 常见问题与调试排错手册
5.1 求解器突然报“不可行”怎么排查
模型不可行是我做过那么多调度模型以来遇到最多的坑。通常发生在你新加了一条约束后,或者修改了某些参数的边界。出现不可行时,先别急着删约束,按顺序排查。
先把所有决策变量的上下界打出来,看看是不是某个变量的界本身矛盾。比如,如果load[t]在某个时段是0,需求响应的平移上限又给了较大的值,功率平衡约束就怎么都凑不出来。这时把Punmet的惩罚系数调高但仍然保留松弛能力,通常能快速解决问题。
第二步,逐条注释掉约束,观察哪条被注释后模型变成可行。这个方法土但效率高。我用得最多的是“分模块测试”:先跑纯功率平衡和储能约束,确认没问题后再加入需求响应约束,再确认购售电互斥约束,逐层叠加。这样一旦模型变为不可行,问题定位就非常快。
5.2 为什么结果里出现了储能同时充放电
这个现象我在早期做线性版本的模型时遇到过。没有加0-1互斥变量之前,模型为了满足特定约束,确实可能同时让Pch和Pdis都大于零。理论上因为充电效率和放电效率都小于1,同步充放会白白损失能量,目标函数应该自动避免。但当你引入其他强约束时,某些极端场景下模型可能会“不得不”这样做来满足功率平衡。
解决办法也简单:加两个0-1变量,分别表示充电状态和放电状态,约束Pch与状态变量的关系,放电同理,再加两者之和不超过1。示范代码如下:
m.y_ch = pyo.Var(m.t, within=pyo.Binary) m.y_dis = pyo.Var(m.t, within=pyo.Binary) # 互斥 def mutex_rule(m, t): return m.y_ch[t] + m.y_dis[t] <= 1 m.mutex = pyo.Constraint(m.t, rule=mutex_rule) # 功率与状态关联 def ch_state(m, t): return m.Pch[t] <= Pbat_max * m.y_ch[t] m.ch_state = pyo.Constraint(m.t, rule=ch_state) def dis_state(m, t): return m.Pdis[t] <= Pbat_max * m.y_dis[t] m.dis_state = pyo.Constraint(m.t, rule=dis_state)5.3 模型结果过于理想,一执行就偏差巨大
这个问题的根源不在模型,而在预测误差。日前调度的输入,比如光伏出力、负荷用电量,都是预测值,真实运行时可以轻松产生20%以上的偏差。如果你的模型没有给不确定性留余量,计划曲线看着很漂亮,实际执行时却处处碰壁。
工程上通常加备用约束:让联络线的购电功率不要顶到上限,给储能留一部分空余容量作为备用,或者在目标函数里对可切负荷设定一个较合理的惩罚,提高模型的“稳健性”。还有一种做法是把日前模型结果作为日内滚动优化的初始条件,每15分钟重新优化一次,把最新的预测和实际状态用进去。也就是说,日前计划不是一成不变的圣旨,而是一份凌晨出发时的导航路线,途中要根据交通情况实时调整。
5.4 求解时间越来越长怎么办
模型规模大了之后,求解时间确实会从几秒涨到几分钟。最常见的原因有两个:一是整数变量太多,二是大M约束太多导致模型数值性能下降。
如果遇到求解时间暴涨,先检查是否能降低时间分辨率。比如从15分钟回到1小时,变量数量直接降到四分之一。其次,给求解器设置合理的间隙容忍度,也就是MIPGap,比如1%或者0.5%,这通常能大幅缩短求解时间,而实际成本偏差可以忽略不计。再者,提供一个良好的初始可行解也能加速求解,比如直接用不含整数变量的线性松弛结果作为起点。
说到底,日前调度模型的目标是给决策提供指引,不是找数学意义上的全局最优解。一个在1%误差范围内、几分钟内能跑出来的次优解,往往比挂机半小时的精确最优解更有工程价值。
6. 运行效果与扩展建议
6.1 灵敏度分析:哪种参数对成本影响最大
模型跑通之后,下一步我强烈建议做灵敏度分析,而不是直接进入扩展。简单说就是单独改变一个参数,观察总成本的变化幅度。比如把需求响应最大比例从10%提高到20%,成本下降多少?把储能效率从0.9提升到0.95,每年能省多少电费?把储能功率上限从50kW提高到80kW,会不会改善削峰瓶颈?
通过这种分析,你能知道项目继续投入的“边际收益”。从我做的算例来看,影响最大的往往是储能容量和谷峰电价差:当峰谷价差大时,储能的价值显著放大;当需求响应比例低时,储能是主要调节手段;当需求响应比例提高时,储能的循环次数会降低,电池寿命相应延长。这些关系对投资决策非常有价值,比单个最优结果本身更有说服力。
6.2 扩展方向:冷热电联供与不确定性建模
如果你需要在现有框架上继续做大,有两个方向比较值得尝试。一个是加入冷热电联供设备,把电、热、冷三种能源统一建模,功率平衡约束变成多个能量母线的耦合约束,目标函数里还要加上运维成本、CO2排放成本等。体系会更加庞大,但建模思想完全一致。
另一个是不确定性建模。日前预测误差无法回避,可以用随机规划或鲁棒优化的框架来处理。比如用两阶段随机规划,第一阶段决定储能充放电计划等“现在必须定”的变量,第二阶段根据风光和负荷的实际场景调整购售电。这种扩展在学术上很成熟,在工程上也越来越受到重视。但要注意,随机规划的场景数和求解时间会快速增长,场景缩减技术比如基于概率距离的场景削减算法会派上用场。我建议先把确定性模型的逻辑吃透,再往不确定性方向走,否则很容易被复杂的模型结构绕晕。
6.3 部署与复盘建议
最后说说落地部署的经验。模型做出来之后,开发环境运行正常并不等于现场能用,主要差距在数据接口和异常处理。现场数据采集会有延迟、丢失、坏点,预处理的职责必须做好:对负荷、风光数据做合理性校验,对缺失值做填充,这些环节在整个链路里占据的工作量真的不少。
每天滚动运行的流程建议这样安排:凌晨零点用最新数据刷新一次日前计划;每小时再用实测数据做一次日内修正;调度执行后把实际结果记录下来,反馈给模型做第二天的参数校准。这种滚动方式下,日前调度的价值就真正体现出来了。另外,一定要给模型结果加“人工复核”环节,尤其是出现极端的储能行为或者需求响应量时,宁可手动调整一下参数重跑,也不要让异常方案直接下发到执行层。
以我个人经验,从零到一跑通这套风光储能加需求响应的日前经济调度模型,花的时间大概在一周左右,其中数据整理占了差不多一半。模型结构本身并不复杂,真正的难点在于把每个约束的工程含义想清楚,才能写出经得起检验的代码。希望这篇实战笔记能帮想入局微电网优化的朋友少踩一些我踩过的坑。