资讯动态

Python实现风光储互补调度:电池与废弃矿井抽蓄协同优化

发布时间:2026/10/3 14:23:56 来源:尧图企业网站定制
1. 这个课题到底在解决什么问题新能源圈子里风电、光伏装机越上越多大家聚在一起聊得最多的已经不是“怎么发”而是“发了怎么用”。风光出力天然跟着天气走晴天和阴天、大风和静风之间的出力可以差出好几倍而负荷侧的用电曲线又稳定得近乎固执这就造成一个非常实际的矛盾风电光伏大发的时候电网消纳不了只能白扔——弃风弃光就是这么来的反过来风光出力不足的时候又要靠火电或者外购电来顶上碳排放和成本双双起飞。我这次做的课题核心思路就是把“储能”这块拼图放进去让风电、光伏和储能形成一套互补调度系统。但这里有个很多人容易忽略的点储能不是只有一种电池储能和抽水蓄能的特性完全不同把它们放在一起协同调度效果远比单独堆一种储能要好。特别是“废弃矿井小型抽水蓄能”这个方向这两年学术和工程圈子里讨论得非常多把资源枯竭的矿井巷道和竖井改造成抽蓄电站既解决了储能容量问题又盘活了废弃资产属于典型的把包袱变成资源。这个课题适合谁看首先是做电力系统优化调度的研究生或者工程师你需要一套可以直接跑的数学模型和Python代码框架其次是新能源电站的管理者你想搞清楚在配了不同种类储能的情况下调度策略到底怎么定最划算最后是刚入门Python、想拿一个真实综合性能源课题练手的同学——因为这套代码麻雀虽小五脏俱全涉及数据预处理、数学建模、优化求解和可视化一次全流程走下来对Python的理解会上一个台阶。我这次用Python把“风电光伏电池储能废弃矿井小型抽水蓄能”的互补调度模型完整跑通了模型用的是线性规划的思路在时间尺度上把电池的快速响应特性和抽蓄的大容量、长周期特性做了协同分配。接下来我把整个思路、数学模型、代码实现和踩坑记录从头到尾拆开讲。2. 调度优化模型的数学骨架2.1 目标函数系统运行成本最小化任何调度问题第一步都是确定“优化什么”。在多能互补系统里最通用的目标函数是系统总运行成本最小化。如果没有配火电机组运行成本主要来自外部购电费用如果配了火电还得加上燃料成本和启停成本。此外我习惯在目标函数里加入惩罚项用来约束“弃风弃光”和“失负荷”——这两个指标本质上是靠惩罚系数压下去的。目标函数的数学形式可以写成$$min \sum_{t1}^{T} \left[ C_{buy}P_{buy,t} C_{Op}^{bat}(P_{ch,t}P_{dis,t}) C_{Op}^{pump}(P_{gen,t}P_{pump,t}) C_{cut}P_{cut,t} C_{loss}P_{loss,t} \right]$$这里面每一项都对应一个物理量和它背后的成本逻辑。买电成本是大头占比最高储能运维成本虽然单价不高但因为充放电功率循环次数多累积起来也不可忽视弃风弃光惩罚系数和失负荷惩罚系数的取值通常会比正常购电单价高一截否则优化器会倾向于“弃掉算了”和“少送点电”结果做出来虽然是成本最优但实际上物理上不可用。这个目标函数的结构其实代表了调度系统的价值取向——是偏重经济性还是偏重可靠性还是偏重新能源消纳。想调权重改惩罚系数就行。例如想把弃光率压到更低就把弃光惩罚系数调高优化器自然会多调用储能来平移出力牺牲一些储能循环寿命成本来换取更高的新能源利用率。2.2 核心约束条件拆解模型里的约束条件才是真正体现物理规律的部分也是新手最容易写崩的地方。我整理一下核心约束的类别第一类是功率平衡约束也是最根本的约束。任意一个时段t整个系统的发电、购电、储能放电之和必须等于负荷、储能充电、弃电之和$$P_{wind,t} P_{pv,t} P_{buy,t} P_{dis,t} P_{gen,t} P_{load,t} P_{ch,t} P_{pump,t} P_{cut,t}$$第二个是电池储能约束。这里必须引入SOC荷电状态的递推关系$$SOC_{t1} SOC_t \left( \eta_{ch}P_{ch,t} - \frac{P_{dis,t}}{\eta_{dis}} \right) \Delta t$$同时SOC要满足上下限。这里有个新手常踩的坑充放电效率处理不当。充电时是一个小于1的系数做乘法放电时是相当于除以一个小于1的系数放掉的电量比SOC的减少量要大对应放电过程中的能量损耗。很多人把两个效率用反了结果电池用着用着电量凭空变多一看就是错误的。第三个是抽水蓄能的水量平衡约束。抽蓄本质上是一个“能量在重力势能和水容积之间转换”的系统它的状态变量是上水库水量 $V_{up,t}$递推式$$V_{up,t1} V_{up,t} \left( \eta_p P_{pump,t} \cdot \frac{3600}{gH} - \frac{P_{gen,t}}{gH\eta_g} \cdot 3600 \right) \frac{1}{A_{res}}$$这里稍微绕一些但理解起来也不难抽水时电能转成水的重力势能水泵效率让一部分能量损耗掉了发电时水的重力势能又转换成电能水轮机效率同样有损耗。因为废弃矿井抽蓄的水头通常比较低、库容也有限所以库容边界约束必须严格加上不能像大型抽蓄那样当作无限水库处理。第四类是状态互斥约束。电池不能同时充电和放电抽蓄不能同时抽水和发电这两组约束需要引入0-1整数变量来强制。这也是这个模型变成MILP混合整数线性规划的原因。最后一个就是风光出力上限约束。风电光伏在调度里通常按预测出力作为上限调度结果只能小于等于预测值多出来的部分就是弃电。或者反过来把风光出力作为已知参数直接代入功率平衡弃电量显式表达为决策变量。2.3 为什么用优化而不是规则判断很多没做过调度的人第一个反应是风光不足就放储能风光多了就充储能这不很简单吗还用得着什么数学模型我一开始也觉得规则判断就能搞定直到真正跑起来才发现当系统里有电池、有抽蓄、有购电、有弃电约束四五个决策维度叠加在一起的时候规则表的复杂程度指数级上涨而且你根本没法证明“这条规则在某个极端天气下还是最优的”。优化方法的核心优势在于它把所有可行方案放到一个数学框架里统一搜索带约束、带目标求解出来的结果天然是全局最优或者接近最优的。调度问题的本质就是“在给定约束下找一组决策变量的最优组合”这不就是最优化问题嘛。用优化模型来写调度策略逻辑上是更干净、更可解释的。不过要注意常规的确定性优化假设风光出力是已知的确定值这在实际运行中是有偏差的。所以在做工程部署的时候通常会加上滚动优化或者鲁棒优化的外壳——每个时段都重新求解把最新的预测数据输进去。这方面的扩展我会在最后提一下。3. 用Python把这套模型跑起来3.1 基础数据准备负荷、风光出力与储能参数做优化调度之前数据准备工作大概要占整个项目三分之一的时间这一点很多人没有预料到。我用的数据结构是这样的用pandas按15分钟一个间隔构建时序索引一天96个点一周就是672个点。每个时间点对应一组负荷、风电出力、光伏出力单位统一折算成MW。关于单位统一这件事我必须强调一下MW是功率单位MWh是电量单位在递推公式里一旦时间间隔不为1小时功率和电量之间的换算就非常容易出错。15分钟间隔下功率乘以0.25小时才是这15分钟内的电量。储能参数建议整理成一张参数表我用的是字典结构存参数方便随时调。电池那边需要额定容量MWh、最大充放电功率MW、充放电效率、SOC上下限和初始SOC废弃矿井抽蓄那边需要上水库有效库容、水头落差值、水泵效率和发电效率。这些参数的获取要么查设备铭牌要么找废弃矿井地质资料。用废弃矿井做抽蓄有个比较特殊的地方它的库容受巷道空间限制往往只有几万到几十万立方米在大电网尺度下算是典型的“小型”抽蓄单机容量通常只有几兆瓦到几十兆瓦。但它的优势在于成本结构低——矿井本身不用挖上下水库的落差已经天然存在。唯一要做的就是在巷道里加密封段防止渗漏。在实际建模中我建议对数据先做一次去量纲处理把所有的功率值都除以基准容量比如系统峰值负荷让变量数值落在0-1之间这样求解器的数值稳定性会好很多。不然电池SOC是0.98、抽蓄水量是50000立方米、买电功率是350MW这几个数量级完全不一样的变量放在同一个模型里求解器容易在数值计算上出问题。3.2 用PuLP构建含多储能互补的调度模型优化建模我选择的是Python里的PuLP库理由有三点第一是语法简单贴近线性规划的数学语言只要会写方程就能把模型搭出来第二是开源免费配套的CBC求解器装好就直接能跑不需要额外配license第三是它跟pandas的兼容性做得不错数据从DataFrame里取出来可以直接喂给模型。先定义决策变量import pulp as pl # 时段数 T 96 # 1天15分钟一个点 # 决策变量 P_buy pl.LpVariable.dicts(buy_power, range(T), lowBound0) P_ch pl.LpVariable.dicts(bat_charge, range(T), lowBound0) P_dis pl.LpVariable.dicts(bat_discharge, range(T), lowBound0) SOC pl.LpVariable.dicts(soc, range(T), lowBound0, upBound1) P_gen pl.LpVariable.dicts(pump_generate, range(T), lowBound0) P_pump pl.LP Variable.dicts(pump_pumping, range(T), lowBound0) V_up pl.LpVariable.dicts(upper_reservoir, range(T), lowBound0, upBoundV_max) x_bat pl.LpVariable.dicts(x_bat, range(T), catBinary) x_pump pl.LpVariable.dicts(x_pump, range(T), catBinary)接下来是约束条件。功率平衡要先写出来prob pl.LpProblem(Multi_Energy_Dispatch, pl.LpMinimize) for t in range(T): prob (P_wind[t] P_pv[t] P_buy[t] P_dis[t] P_gen[t] P_load[t] P_ch[t] P_pump[t] P_cut[t])电池SOC递推约束for t in range(1, T): prob (SOC[t] SOC[t-1] (eta_ch * P_ch[t-1] - P_dis[t-1] / eta_dis) * dt) prob (SOC[0] SOC_init) prob (SOC[T-1] SOC_init) # 周期约束保证每天从同一状态开始抽蓄水量递推约束稍微复杂一点核心是把功率和水量的换算关系用系数K表示K_pump 3600 / (g * H) / eta_p # 抽水功率转水量 K_gen 3600 / (g * H) * eta_g # 发电电量转水量 for t in range(1, T): prob (V_up[t] V_up[t-1] (K_pump * P_pump[t-1] - K_gen * P_gen[t-1]) * dt) prob (V_up[0] V_up_init) prob (V_up[T-1] V_up_init) # 至少保证水量不恶化状态互斥用大M法实现。电池的充放电互斥约束写成M 1e6 for t in range(T): prob P_ch[t] P_bat_max * x_bat[t] prob P_dis[t] P_bat_max * (1 - x_bat[t])抽蓄同理用x_pump控制抽水态和发电态的互斥性。这个写法是MILP建模里的标准做法本质上是说“如果你想放电那么充电决策变量就强制为0反过来也一样”。注意这个M的取值比最大的功率限量大个10倍就行太大了反而会引起数值问题。3.3 结果可视化与对比分析模型求解完以后提取结果我习惯直接放到DataFrame里然后画三张图第一张是整个调度周期的功率平衡堆叠图能直观看到每一刻的电都从哪里来、用到了哪里第二张是电池的SOC曲线和抽蓄的水量曲线对比两种储能的运行节奏第三张是弃风弃光电量的累计曲线用来评估系统的消纳水平。画图用matplotlib堆叠图用stackplot曲线的颜色我习惯统一用一套色板保证在报告里看起来协调。要注意的一点是15分钟间隔的数据画出来会比较密集x轴的刻度建议自己定义间隔不然会糊成一团。import matplotlib.pyplot as plt import matplotlib.dates as mdates fig, ax plt.subplots(figsize(14, 6)) ax.stackplot(df.index, df[wind_used], df[pv_used], df[buy], df[bat_dis], df[pump_gen], labels[Wind, PV, Grid Buy, Battery Discharge, Pump Storage], colors[#1f77b4, #ff7f0e, #2ca02c, #d62728, #9467bd]) ax.set_ylabel(Power (MW)) ax.xaxis.set_major_locator(mdates.HourLocator(interval6)) plt.legend(locupper left, ncol2) plt.tight_layout() plt.show()光看功率平衡还不够要评价系统好不好我把几个关键指标算出来对比包括弃风弃光率、储能利用率、系统综合运行成本和负荷满足率。这几个指标一摆下来方案之间的优劣高下立判。4. 典型场景算例与结果解读我搭了一套典型的算例来验证模型的有效性场景设计成一个靠近沿海的工业园区微电网风电容量80MW、光伏容量50MW、峰值负荷100MW。储能侧配置10MW/20MWh电池和一座利用废弃矿井改造的抽蓄电站装机20MW、上水库有效库容50万立方米、水头差60米。场景覆盖夏季的过渡季节一周风光出力波动明显。先单独跑电池储能方案做基线再跑电池抽蓄互补方案。两条结果一对比差异非常明显纯电池方案下因为电池容量有限下午光伏大发的时候只能靠电池硬扛到傍晚SOC已经顶到上限接下来晚高峰时段完全没有剩余容量来支撑只能大量从电网买电累计弃光率大约7.8%而互补方案中光伏大发时段优先用抽蓄把水抽到上水库用“水量”把能量储存下来电池只在更短时间尺度上做响应——从高负荷曲线跳变的时候快速顶上去到了夜间负荷低谷再给电池充电。一周下来弃光率压到3.2%购电费用下降了约12%。这个算例最有意思的观察点在于两种储能在调度结果里自动形成了清晰的分工。电池的SOC曲线呈高频锯齿状频繁充放但每次充放电量不大抽蓄的水量曲线则呈平滑的日周期波动午后爬升、傍晚下落。这就是“时间尺度互补”的直观呈现——电池擅长处理分钟级的波动抽蓄擅长跨时段的能量转移两者配合每个储能单元都能在自己最舒服的工况下运行。另外我把抽蓄电站从废弃矿井改造带来的成本优势做了敏感性分析抽蓄的单位造价如果是电池的三分之一到五分之一那么在10年运行周期里互补方案的全生命周期成本会显著低于纯电池方案。当然这里要说明一下这个结论依赖具体的场地条件和煤电替代成本但方向上是有说服力的。5. 我实际踩过的几个坑排查与避坑5.1 常见问题速查表做这个课题的过程中我踩了不少坑有些是代码级的低级错误有些是物理概念上的理解偏差挑几个典型问题做成速查表希望能帮大家少走点弯路。问题现象原因解决方法求解器报“No feasible solution”约束条件过严比如库容太小但功率上限太大导致水量装不下先去掉水量边界和SOC边界确认模型是否能解再逐步加回约束定位问题结果出现SOC不守恒充放电效率系数用反了充电SOC增量充电功率×效率放电SOC减少量放电功率/效率回头检查递推公式求解时间过长几百秒以上MILP的整数变量太多96个时段就有192个0-1变量先改成纯LP松弛看看解的变化程度或者把双变量互斥改成单变量三状态编码-1抽水/0停机/1发电结果在某个时段出现“突刺”惩罚系数设置不合理或者功率平衡约束没包含所有项检查目标函数里是否漏写了弃电或者失负荷项重点看功率平衡方程左右两边是否严格相等数值警告“infeasible”但代码没报错变量数量级差异过大兆瓦级别和库容立方米级别混在一起把所有物理量归一化到同一数量级或用标幺值建模5.2 几个值得注意的细节第一废弃矿井抽蓄的建模不能直接照搬常规抽蓄电站的模型。常规抽蓄电站的水头高、库容大调度中很少关心库容边界但废弃矿井巷道库容有限在低谷时段如果大量抽水很容易碰到库容上限而强制停机。所以库容的动态变化一定要建模而且库容初始状态要当作决策的一部分来优化而不是拍脑袋定一个数。第二周期约束的写法直接影响结果的合理性。我建议在实际运行时初始SOC和末尾SOC保持一致或者至少使末尾SOC不低于初始值这样调度结果是可持续的不会出现“第一天把电池电量吃光、第二天重新从满电开始”这种脱离实际的情况。加上这条约束后电池的调度行为会自然变得更健康。第三惩罚系数的取值没有绝对的对错要看你的优化目标是什么。如果做的是以消纳率为核心指标的方案对比就把弃电惩罚系数定得极高强制消纳如果做的是经济性评估惩罚系数应反映真实的电价信号。要记住这个模型本质上是一个“目标函数约束”的求优器你给它什么目标它就会给你什么答案。第四如果想要模型更贴近工程实际不要只做单次优化。建议加载最新的预测数据用滚动时域的方式每隔15分钟或1小时重新求解一次把已经发生的实际功率作为固定值代入本时段再对未来一段时间的决策变量做优化。这种做法的本质是把不确定性通过滚动刷新来抵消结果会鲁棒很多。写在最后跑完这套互补调度模型我最大的体会是多能互补的关键不在“多”而在“互补”——不同储能之间有本质特性差异用对了场景就是补用错了场景就是浪费。电池像短跑选手爆发力强但续航短抽蓄像长跑选手反应慢但耐力足。团队协作中短跑选手去拼抢开局长跑选手来稳住全局整场比赛的收益自然就上来了。最后再分享一个小技巧做完基础的确定性优化之后可以去读一读目标函数里各项成本的比例关系。如果某一天买电费用占比达到85%以上说明储能的容量配置可能偏小了如果储能运维费用占比异常高说明调度策略过于激进储能循环次数太多。这些从结果反推配置的思路比盲目改参数要靠谱得多。这个模型后续还能往碳约束、绿电交易、需求响应等方向扩展代码框架不用大改把对应的约束和目标加进去就行。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑