☰
基于Python的风光储互补优化调度:电池与废弃矿井抽蓄协同建模
2026/10/2 21:21:55 网站建设 项目流程

做新能源调度这一行的人,估计都经历过这样的场景:风电和光伏的数据拿在手里,总电量其实不少,可一到白天负荷尖峰、晚上风大没负荷的时候,电网就是不愿意要你这口电。弃风弃光、功率波动、调峰压力,单拎一个出来都够写一篇论文。我这次想聊的项目,核心就一句话:把风电、光伏、电池储能,以及一个利用废弃矿井改造的小型抽水蓄能电站,放在同一个优化模型里做互补调度,并用 Python 写出一套能跑、能出图、能算指标、能支撑试验的代码。很多朋友一看到“风电、光伏、储能互补调度”就以为一定要 Gurobi、CPLEX 加一大套平台,其实用开源的 PuLP 配合 pandas、matplotlib,完全能把一个 24 小时或更长时间尺度的调度问题做清楚。这篇内容适合正在做电力系统课程设计、毕业论文,或者刚入行做新能源消纳研究的工程师参考,我也尽量把工程上最容易踩坑的细节写出来。

为方便理解,我先把整个项目的解决思路拆开讲:为什么一定要“互补”,电池和废弃矿井小型抽水蓄能各自的角色是什么,然后给出完整的数学建模和 Python 代码实现,最后聊聊调试过程中那些让人头大的问题。文章里给的代码不是玩具,改一改数据和时间尺度就可以直接用到项目里。

1. 为什么要把电池和废弃矿井抽蓄放在同一个优化器里

1.1 风光出力不是“想调就能调”

风电的典型特点是夜间出力偏高,尤其在北方地区,后半夜风速起来、负荷却很低,形成典型的反调峰。如果系统里只有火电或没有足够的灵活性电源,风电就只能在低谷时段被限制。光伏则完全相反,白天中午出力达到峰值,早晚和夜间出力接近零,而且随着天气变化,云层遮挡会让出力在几分钟内剧烈波动。把风电和光伏放在一起看,确实存在一定的时间互补性,但互补并不等于稳定,两者叠加后依然会有大幅波动。

要平抑这种波动,需要两类资源:响应速度快、能够处理高频波动的电源或储能;能量容量大、能够在更长的时间尺度上搬移能量的电源或储能。单一储能往往顾此失彼。比如电池储能虽然响应速度快,但容量成本高,不适合连续十几个小时的大规模电量搬移;传统大型抽蓄容量大,但建设周期长、站点资源有限,不是每个地方都有条件建。

1.2 电池与小型抽蓄的互补画像

电池储能和抽水蓄能在电力系统里的定位,可以理解成一个短跑运动员和一个长跑运动员。电池擅长“削刺”,能够在秒级到分钟级完成充放电切换,平抑光伏云层遮挡带来的突然跌落,或者风电阵风带来的功率尖峰。但电池的持续放电时间通常只有 1 到 4 小时,能量密度有限,而且循环次数和放电深度强相关,频繁深度充放电会加速容量衰减。

小型抽水蓄能则像一个“能量搬运工”,它可以把白天光伏大发时段的电量转换成水的势能存起来,等晚高峰再放出来。这个过程不需要昂贵电池材料,设备寿命很长,容量也可以做得比较大。但常规抽蓄对地理条件要求高,需要上下水库和较大落差,而废弃矿井恰好提供了这种可能性:矿井本身的竖井可作为引水通道,地下巷道可作为水库,井筒落差直接对应水头。这样的小型抽蓄单机规模不大,但对分布广泛的矿区来说,是很现实的一种储能改造方案。

下面是两者在调度模型中最关键的特性对比:

特性电池储能废弃矿井小型抽水蓄能
响应速度秒级到分钟级分钟级到十分钟级
持续充放电时间通常 1-4 小时可做到 4-10 小时甚至更长
循环寿命与充放电深度相关,数千次机械寿命长,数十年
效率单程 90%-95%,往返约 85%-90%单程约 85%,往返约 70%-75%
选址限制相对灵活,可模块化布置需要废弃矿井或类似地形
主要作用高频调节、快速响应、维持功率平衡大容量能量搬移、削峰填谷、提高消纳空间

这两个储能放在同一个优化模型里,并不是简单相加,而是要协同分配:高频的小幅波动尽量用电池去处理,低频大幅度的能量搬移靠抽蓄去承担。如果只用电池,可能容量不够;如果只用抽蓄,风光出力的快速波动又会让机组频繁启停,对设备很不友好。

1.3 废弃矿井抽蓄不是天方夜谭

很多人听到“废弃矿井小型抽水蓄能”第一反应是工程上不太现实,其实国外和国内都做过不少前期研究。矿井被废弃后,地下巷道和硐室只要经过稳定性评估和防渗处理,就可以改造成蓄水空间。利用矿区的竖井落差,在地面或附近建一个低位水池,再配合原有的井筒巷道形成水位差,就能实现抽水和发电循环。这种方案可以盘活大量废弃矿区资产,同时避免矿井关闭后地面塌陷、地下水位上升等次生问题。

当然,这种小型抽蓄也有明显约束:可用库容受巷道空间限制,水头受矿井深度限制,选址依赖于具体矿区,距离负荷中心和电网节点也可能比较远。所以在调度模型里,它通常被抽象成一个带最大库容、最大抽水功率、最大发电功率和效率转换的“等效能量存储系统”。这样抽象并不丢失核心物理规律,反而便于和电池统一建模。

2. 调度模型:把物理场景翻译成线性约束

2.1 目标函数:成本、弃电和运行损耗怎么加权

调度运行的第一个问题是:我们到底想优化什么。工程项目里不可能只看弃风弃光率,也要看购电成本、储能运行成本和是否切负荷。我常用的做法是把这些目标线性加权,公益化和经济化之间用惩罚系数平衡。以并网模式为例,目标函数可以写成:

$$ \min \sum_{t=1}^{T} \left( c_{buy,t} , P_{buy,t} + c_{cur}^{w} , P_{cur,t}^{w} + c_{cur}^{pv} , P_{cur,t}^{pv} + c_{op}^{b} , (P_{ch,t}^{b}+P_{dis,t}^{b}) + c_{op}^{p} , (P_{pump,t}+P_{gen,t}) \right) $$

其中每一项的含义是:购电费用、风电弃电惩罚、光伏弃电惩罚、电池运行维护成本、抽蓄运行维护成本。这样写的好处是,所有目标都变成一个总成本,可以直接交给混合整数线性规划求解器。弃电惩罚系数的设置要高于平均购电价格,通常设为购电最高价格的 1.2 到 1.5 倍,这样模型会优先消纳风、光,而不是简单选择从电网购电。如果你更关注“新能源利用率”,可以把弃电惩罚设得很大,但注意这会让模型只追求消纳而忽视经济性,最终结果可能很不贴合实际运行。

罚系数的选择没有绝对标准。我的建议是先跑几组不同惩罚值的方案,画出弃电率和运行成本的变化曲线,再结合实际考核要求选一个折中点。这样比拍脑袋定系数靠谱得多。

2.2 功率平衡与设备物理约束

模型里的约束分为几类。第一类是系统功率平衡,这是任何调度模型都不能少的约束,它要求每个时段所有电源出力和负荷、储能充电、抽水耗电之和严格相等。在并网模式下,缺电可以从电网购买;在孤岛模式下,缺电就只能切负荷,所以我会在目标函数里加一个非常高的失负荷惩罚,避免模型把切负荷当成常规操作。

第二类是各设备自身的出力上下限和状态约束。电池充电功率、放电功率都有上限,而且充电和放电不能同时进行,这就需要一个二进制状态变量来互斥。抽蓄的抽水功率、发电功率同样有上限,抽水和发电状态也要互斥。在工程实际中,抽水蓄能机组从抽水工况切换到发电工况通常需要几分钟,所以在分钟级调度里还应考虑启停时间,但在小时级日前调度里,这些细节可以暂时忽略。

第三类是储能能量状态的递推约束。对电池来说:

$$ E_{t+1}^{b} = E_{t}^{b} + \eta_{c} , P_{ch,t}^{b} , \Delta t - \frac{P_{dis,t}^{b}}{\eta_{d}} , \Delta t $$

抽蓄的能量递推关系类似:

$$ E_{t+1}^{p} = E_{t}^{p} + \eta_{pump} , P_{pump,t} , \Delta t - \frac{P_{gen,t}}{\eta_{gen}} , \Delta t $$

同时,储能能量有上下限,最后时刻和初始时刻的能量一般要相等,这是为了体现“日内调度是循环运行”的特点。如果不加这个约束,模型会倾向于在最后时段把所有存储能量放空,第二天无法继续运行,结果就失真了。

2.3 为什么选线性规划而不是深度学习

现在很多朋友一上来就想到强化学习、LSTM 预测加调度,但实际做工程项目,我依然优先推荐混合整数线性规划。主要原因有三个:一是线性模型的可解释性强,调度方案可以回溯到每个约束,论文答辩时也容易讲清楚;二是工业和学术界对 MILP 的求解已经非常成熟,几十个变量、几百个约束的问题在普通笔记本上几秒钟就能解出来;三是强化学习面对强约束和模型不确定性时,还需要大量训练和调参,短期内未必比直接优化更好。

Python 里做 MILP 的选择很多,商业求解器各有优势,开源方案里 PuLP 加 CBC 求解器足够处理中小规模的日前调度问题。如果你需要更快的大规模求解,可以换用 Google OR-Tools 或 HiGHS,核心建模逻辑是相通的。下面的代码统一用 PuLP 演示,因为它在 Windows、Linux 和 macOS 上安装都很简单,对初学者友好。

3. Python 完整实现:从数据生成到结果可视化

3.1 环境与输入数据准备

运行代码前,需要确保本机有 Python 3.8 以上环境,并安装好 pandas、numpy、matplotlib 和 pulp。如果还没有安装这些库,在终端执行pip install pandas numpy matplotlib pulp即可。PuLP 安装时一般会自带 CBC 求解器,不需要额外配置。

没有实测风、光、负荷数据怎么办?这是很多学生做研究时卡住的第一步。我的做法是先用数学方法生成一套接近实际形态的模拟数据,等代码逻辑跑通后再替换成真实数据。下面这段代码生成 24 小时的数据,单位统一为 MW,时间间隔为 1 小时:

import numpy as np import pandas as pd T = 24 dt = 1.0 # 小时 time_idx = np.arange(T) # 模拟负荷:早晚双峰,均值100MW左右,峰值150MW load_base = 110 + 25 * np.sin(2 * np.pi * (time_idx - 7) / 24) load = np.clip(load_base + 10 * np.random.randn(T), 60, 160) # 模拟光伏:只在白天出力,峰值80MW pv = np.zeros(T) sun_hours = np.arange(6, 18) pv[6:18] = 80 * np.sin(np.pi * (sun_hours - 6) / 12) pv = np.clip(pv + 5 * np.random.randn(T), 0, 100) # 模拟风电:夜间高、白天低,峰值150MW左右 wind = 90 + 30 * np.sin(2 * np.pi * (time_idx - 2) / 24) wind = np.clip(wind + 15 * np.random.randn(T), 0, 180) data = pd.DataFrame({ "time": np.arange(1, T + 1), "load": load, "pv": pv, "wind": wind }) print(data.head(10))

生成的数据本身带随机性,但总体形态符合实际:负荷有早晚两个峰,光伏在 6 点到 18 点之间呈钟形,风电在夜间偏高。用这样的数据调试模型,比直接用一堆完全随机的数字要靠谱得多。真实场景中,风电和光伏出力应该来自功率预测系统或典型日曲线,负荷来自当地电网或同类地区统计值。如果数据时间间隔不是 1 小时,比如 15 分钟,一定要把dt改成 0.25,否则储能能量的计算会错得离谱。

3.2 用 PuLP 搭建混合整数线性规划模型

下面这段代码是整个项目的核心。我尽量把变量写清楚,方便你根据自己项目场景调整。假设电池容量是 100MWh,最大充放电功率都是 40MW,充放电效率取 95%;抽蓄的最大抽水功率 40MW,最大发电功率 50MW,抽水效率 85%,发电效率 85%,抽蓄可用能量上限 400MWh、下限 50MWh。

import pulp as lp # 参数设置 BESS_CAP = 100.0 # 电池容量 MWh BESS_PMAX = 40.0 # 电池最大充放电功率 MW BESS_EFF = 0.95 # 电池单程效率 PHES_EMAX = 400.0 # 抽蓄上库可用能量上限 MWh PHES_EMIN = 50.0 # 抽蓄上库可用能量下限 MWh PHES_PGEN = 50.0 # 抽蓄最大发电功率 MW PHES_PPUMP = 40.0 # 抽蓄最大抽水功率 MW PHES_EFF = 0.85 # 抽蓄单程效率 BUY_MAX = 80.0 # 从电网购电上限 MW PRICE_BUY = np.where((time_idx >= 23) | (time_idx < 7), 0.4, 1.0) # 峰谷电价 元/kWh -> 直接按系数处理 CURT_PENALTY = 1.5 # 弃电惩罚系数

建立优化问题:

prob = lp.LpProblem("Hybrid_Storage_Dispatch", lp.LpMinimize) # 购电变量 P_buy = lp.LpVariable.dicts("P_buy", range(T), lowBound=0, upBound=BUY_MAX) # 风电、光伏使用与弃电变量 P_w_use = lp.LpVariable.dicts("P_w_use", range(T), lowBound=0) P_pv_use = lp.LpVariable.dicts("P_pv_use", range(T), lowBound=0) P_w_curt = lp.LpVariable.dicts("P_w_curt", range(T), lowBound=0) P_pv_curt = lp.LpVariable.dicts("P_pv_curt", range(T), lowBound=0) # 电池变量 E_b = lp.LpVariable.dicts("E_b", range(T + 1), lowBound=20, upBound=BESS_CAP) P_b_ch = lp.LpVariable.dicts("P_b_ch", range(T), lowBound=0, upBound=BESS_PMAX) P_b_dis = lp.LpVariable.dicts("P_b_dis", range(T), lowBound=0, upBound=BESS_PMAX) u_b_ch = lp.LpVariable.dicts("u_b_ch", range(T), cat="Binary") u_b_dis = lp.LpVariable.dicts("u_b_dis", range(T), cat="Binary") # 抽蓄变量 E_p = lp.LpVariable.dicts("E_p", range(T + 1), lowBound=PHES_EMIN, upBound=PHES_EMAX) P_p_pump = lp.LpVariable.dicts("P_p_pump", range(T), lowBound=0, upBound=PHES_PPUMP) P_p_gen = lp.LpVariable.dicts("P_p_gen", range(T), lowBound=0, upBound=PHES_PGEN) u_p_pump = lp.LpVariable.dicts("u_p_pump", range(T), cat="Binary") u_p_gen = lp.LpVariable.dicts("u_p_gen", range(T), cat="Binary")

加上约束:

# 电源出力不被使用即为弃电 for t in range(T): prob += P_w_use[t] + P_w_curt[t] == wind[t] prob += P_pv_use[t] + P_pv_curt[t] == pv[t] # 功率平衡 for t in range(T): prob += (P_w_use[t] + P_pv_use[t] + P_b_dis[t] + P_p_gen[t] + P_buy[t] == load[t] + P_b_ch[t] + P_p_pump[t]) # 电池状态转移与互斥 for t in range(T): prob += E_b[t + 1] == E_b[t] + BESS_EFF * P_b_ch[t] * dt \ - P_b_dis[t] * dt / BESS_EFF prob += P_b_ch[t] <= BESS_PMAX * u_b_ch[t] prob += P_b_dis[t] <= BESS_PMAX * u_b_dis[t] prob += u_b_ch[t] + u_b_dis[t] <= 1 # 抽蓄状态转移与互斥 for t in range(T): prob += E_p[t + 1] == E_p[t] + PHES_EFF * P_p_pump[t] * dt \ - P_p_gen[t] * dt / PHES_EFF prob += P_p_pump[t] <= PHES_PPUMP * u_p_pump[t] prob += P_p_gen[t] <= PHES_PGEN * u_p_gen[t] prob += u_p_pump[t] + u_p_gen[t] <= 1 # 初始能量与日内循环约束 prob += E_b[0] == 60 prob += E_p[0] == 200 prob += E_b[T] == E_b[0] prob += E_p[T] == E_p[0]

目标函数:

prob += lp.lpSum(PRICE_BUY[t] * P_buy[t] + CURT_PENALTY * (P_w_curt[t] + P_pv_curt[t]) for t in range(T))

求解并查看状态:

solver = lp.PULP_CBC_CMD(msg=0, timeLimit=60) prob.solve(solver) print("求解状态:", lp.LpStatus[prob.status])

注意timeIdx是 0 到 23 的整数,PRICE_BUY里的time_idx >= 23表示 23 点整也是谷段,实际应用中电价时段按各地规则调整即可。

3.3 结果提取、指标计算和画图

求解完成后,需要把变量从 PuLP 里取出来,整理成 DataFrame,便于进一步分析和绘图。这一步很基础但容易漏,我习惯直接写一个通用函数:

result = pd.DataFrame({ "load": load, "wind": wind, "pv": pv, "wind_use": [P_w_use[t].value() for t in range(T)], "pv_use": [P_pv_use[t].value() for t in range(T)], "wind_curt": [P_w_curt[t].value() for t in range(T)], "pv_curt": [P_pv_curt[t].value() for t in range(T)], "battery_ch": [P_b_ch[t].value() for t in range(T)], "battery_dis": [P_b_dis[t].value() for t in range(T)], "phes_pump": [P_p_pump[t].value() for t in range(T)], "phes_gen": [P_p_gen[t].value() for t in range(T)], "grid_buy": [P_buy[t].value() for t in range(T)], }) result["bess_soc"] = [E_b[t].value() for t in range(T + 1)][:T] result["phes_soc"] = [E_p[t].value() for t in range(T + 1)][:T]

指标计算:

total_renewable = wind.sum() + pv.sum() total_curtail = result["wind_curt"].sum() + result["pv_curt"].sum() curtail_rate = total_curtail / total_renewable total_buy_cost = (result["grid_buy"] * PRICE_BUY).sum() print(f"新能源弃电率: {curtail_rate * 100:.2f}%") print(f"购电总成本: {total_buy_cost:.2f} 元")

画图可以很简单,用 matplotlib 画两个子图,一个是功率平衡堆叠图,一个是储能 SOC 曲线:

import matplotlib.pyplot as plt fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True) hours = np.arange(1, T + 1) axes[0].bar(hours - 0.3, result["wind_use"], width=0.2, label="风电使用") axes[0].bar(hours, result["pv_use"], width=0.2, label="光伏使用") axes[0].bar(hours + 0.3, result["grid_buy"], width=0.2, label="购电") axes[0].step(hours, load, where="mid", color="black", label="负荷") axes[0].set_ylabel("功率 (MW)") axes[0].legend() axes[1].step(hours, result["bess_soc"], where="mid", label="电池SOC") axes[1].step(hours, result["phes_soc"], where="mid", label="抽蓄库容") axes[1].set_ylabel("存储能量 (MWh)") axes[1].set_xlabel("时段 (h)") axes[1].legend() plt.tight_layout() plt.show()

3.4 跑一个 24 小时算例的结果长什么样

用以上模拟数据跑完后,会得到类似下面的规律:白天 10 点到 14 点光伏出力大,如果负荷相对较低,电池和抽蓄同时充电,多余电量全部被储能吸收;到了 18 点到 21 点晚高峰,光伏已经降为零,风电还没有完全升起,此时电池放电、抽蓄放水,购电控制在较低水平。夜间 23 点到凌晨 5 点,风电出力上升而负荷下降,系统又进入储能充电模式,为早高峰做准备。

以下是示意性的输出结果,不同随机种子会产生不同数字,但趋势一致:

时段负荷(MW)风(用/弃)光(用/弃)电池充/放抽蓄抽/发购电(MW)
8:0012852.1/017.4/0-8.20.035.6
11:0010248.3/066.8/08.616.40.0
14:009555.2/055.0/05.220.10.0
20:0014361.3/00.0/025.446.89.5
23:007897.5/00.0/018.941.30.0

负的充放电表示充电,正的是放电。可以看到,电池和抽蓄在不同时段轮流工作,弃电率为 0,购电成本也被有效控制。

4. 调度结果怎么解读:谁在干重活,谁在打辅助

4.1 电池负责“削刺”,抽蓄负责“移峰”

很多人拿到调度结果后只看弃电率,这是不够的。我更关注两类储能在时间轴上如何分工。电池的 SOC 曲线通常呈现频繁上下波动,说明它在不断响应风光出力变化和负荷波动;抽蓄的库容曲线则更平滑,它基本是在“削峰填谷”的大尺度上工作:白天光伏多就抽水蓄能,晚上风力充足但负荷低也蓄能,到了早晚两个负荷高峰再发出来。

如果模型里只有电池,相同场景下电池的 SOC 会长时间顶到上限或压到下限制,不仅影响寿命,还会因为容量不够而产生弃电。如果只有抽蓄,遇到几分钟级的风力突降,抽蓄机组的调节速度跟不上,系统会出现短时功率缺口。因此,我在目标函数里给蓄电池和抽蓄都加了一点运行维护成本,哪怕很小,也会让求解器优先选择合理的工况:快速波动让电池去扛,持续能量搬移让抽蓄去扛。

4.2 并网与孤岛模式的调度差异

并网模式下,调度模型多了购电这个变量,相当于给系统加了安全垫。在风光极差、储能又放空的情况下,模型可以从电网买电,保供电能力更强。孤岛模式下,没有电网支撑,系统只能在“切负荷惩罚”和“多储能容量”之间权衡。我做孤岛算例时发现,如果惩罚系数设置合理,调度结果会更保守,比如夜间风电出力再高,抽蓄也不会把库容蓄得超过 90%,因为要预留早高峰的调节空间。

如果要同时研究两种模式,我建议做一个参数开关,比如用一个布尔变量is_island控制是否生成购电变量。这样一套代码就能覆盖两个场景,后续写报告时还能对比分析。

5. 实战中的坑与排查方法

5.1 模型不可行:大部分是量纲和末端约束问题

我自己调试这个项目时,遇到最多的就是Status: Infeasible。表面看是约束冲突,实际排查时我按这个顺序来:先看所有能量变量的上下限是否和功率变量匹配;再看dt有没有写错;然后检查始末库容约束。有一个很常见的低级错误:时间间隔是 15 分钟,但dt=1,导致储能递推公式算出来的能量是实际值的 4 倍,最终模型认为电池“装不下”足够能量,直接出现不可行。

还有一个隐蔽问题:初始能量约束和周期约束同时加,可能把储能逼到死角。比如要求电池从 60MWh 开始,又在一天内回到 60MWh,但这段期间恰好是连续低谷,风小、光弱、购电上限不够,就没有可行解。这时候可以暂时去掉末端周期约束,跑一次看看;如果去掉后能解,说明是“日净能量”不平衡,要么增加购电上限,要么扩大储能容量,要么放宽初始 SOC。

5.2 求解器卡死或超时怎么办

24 小时模型里变量和约束都很少,CBC 求解器通常秒级收敛。但如果你把模型扩到 365 天、每天 96 个时段,二进制变量会暴增,CBC 可能几分钟甚至更久都出不来。这时候有几个实用办法:

  • 把时间间隔从 15 分钟改成 1 小时,先验证逻辑,再逐步细化。
  • 用滚动调度替代全时段单次优化,比如每 6 小时重新优化一次,每次窗口只取 36 小时,兼顾精度和速度。
  • 换成 HiGHS 或 OR-Tools 求解器,它们在处理较大规模 MILP 时比老版本 CBC 更快。

我实测过,同样的 96 时段模型,用 CBC 和 HiGHS 求解,时间差经常在 5 倍以上。如果代码是用 PuLP 写的,换求解器通常只改一行solver,代价很低,值得一试。

5.3 弃电率不是唯一的评价指标

很多人喜欢把弃电率压到 0,认为这是最优。但如果你给弃电惩罚设得极高,模型可能会让电池和抽蓄在低谷时段拼命充电,造成不必要的循环老化,甚至为了减少弃电,让系统在不需要的时候也囤积大量能量。这个结果虽然好看,却不经济。

我在项目里会同时看三个指标:弃电率、运行成本、储能循环次数。只有在三者之间权衡出一个可接受的方案,才敢拿去支撑结论。实际操作时,可以跑几组不同惩罚系数的方案,生成一张弃电率-成本曲线,类似帕累托前沿,这样既能看出系统的真实潜力,也能让论文有更多讨论空间。

6. 再往前走一步:滚动调度和多目标扩展

6.1 把日前调度改成滚动调度

日前调度的前提是未来 24 小时的预测完全准确,但风电和光伏的预测误差会随时间推移而累积。工程上更常用的是滚动调度:每过 1 小时或每过 4 小时,用最新的预测数据重新优化未来 12 小时到 36 小时的计划。这样调度的动作会根据天气变化动态修正,电池和抽蓄的充放电计划也不会一条路走到黑。

在代码上,滚动调度需要把上面的优化核心包在一个循环里。每个周期只取下一段窗口的数据,求解后只执行第一个时段的指令,然后滚动到下个时段。窗口长度和滚动间隔需要配合,比如间隔 1 小时、窗口 24 小时。这样做还能顺便观察预测误差对结果的影响,甚至可以作为鲁棒性分析的一部分。

6.2 多目标与帕累托分析

如果想让研究更有深度,可以把“运行成本最小”和“新能源弃电率最小”作为两个并列目标。直接求双目标需要多目标优化算法,比如用 pymoo 库跑 NSGA-II。不过我觉得做工程研究时,更简单的办法依然是把弃电惩罚系数当作“成本标签”,扫一遍系数得到一条弃电率-成本曲线,这条曲线的拐点就是可选的折中方案。

举个例子,惩罚系数从 0.2 逐渐增加到 2.0,弃电率会从 8% 一路降到 0.5%,但购电成本未必同步下降,因为储能设备的循环损耗也在增加。当惩罚系数超过某个阈值后,弃电率的下降幅度会变得非常小,这时候继续加大惩罚系数就没有意义了。把这条曲线放进论文或技术报告里,比只写一个最优解要更有说服力。

写在最后的一些体会

做这个项目最深的感受是,真正困难的并非求解,而是把物理问题变成数学约束的那一步。只要你把设备参数、效率、时间尺度、边界条件弄清楚了,调度模型本身并不神秘。我在实际开发中踩过几次坑之后,现在养成一个习惯:先跑一个 2 小时缩略模型验证全部变量定义正确,再扩展到完整算例,最后才进行长周期和灵敏度分析。这样排查问题快很多。

如果你是第一次写类似的储能调度代码,建议先不要追求复杂的场景,把 24 小时单目标模型跑通、出图、指标算明白,再逐步加约束、换数据、扩展滚动窗口。等你把电池和抽蓄的互补规律摸透了,再去做多目标、不确定性分析,会顺手很多。

还有一个小技巧:把模拟数据的随机种子固定下来,比如np.random.seed(42),这样每次跑出来的结果一致,方便对比不同参数的影响。否则你调一次参数换一组随机数,根本分不清效果是参数带来的还是数据波动带来的。研究嘛,控制变量永远应该放在第一位。

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

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

立即咨询