做风电、光伏与储能互补调度运行研究这个课题,最开始我以为是数学建模加优化求解的常规组合,真正动手才发现,从风光出力数据到储能系统建模,再到最后的Python代码实现,每一步都藏着不少隐性坑。这篇文章就是做完整个项目之后的完整复盘,我会把从问题定义、模型搭建、算法选择到代码落地的全过程都拆开讲一遍,包括那些踩过坑的地方,希望能帮到正在做新能源调度方向研究的学生,以及想入门电力系统优化调度算法开发的工程师朋友。
整体来看,这个课题最核心的价值在于它同时处理了两个维度的问题:一个是风光出力的随机性和波动性怎么通过调度来平抑,另一个是两种特性完全不同的储能设备(电池和废弃矿井小型抽水蓄能)怎么协同配合。后者是这个题目最有意思的地方,也是很多论文里没有展开讲的部分。
1. 项目背景与整体思路拆解
1.1 风光储互补调度到底在解决什么问题
风电和光伏的出力都是"看天吃饭":风电受风速影响,白天风速往往偏小,夜间才可能起来;光伏只在有光照的时段出力,夜间直接归零。两个电源单独看都不可控,叠加在一起虽然能缓解一部分波动——比如白天光伏出力大、夜间风电出力大的天然时间互补——但远达不到稳定供电的水平。
储能在这个系统里扮演的角色,简单说就是"时间搬运工":把出力过剩时段的多余电能存起来,放到出力不足或负荷高峰的时段释放。但储能本身也有讲究,电池储能响应快、效率高,但容量相对有限,而且频繁深度充放会加速寿命衰减;抽水蓄能容量大、寿命长,可一旦启停,响应速度就差了不少。把这两种储能放到同一个调度模型里,让它们各干各的擅长部分,才是这个课题的真正难点。
这里要特别说下废弃矿井小型抽水蓄能。常规抽水蓄能需要天然的地形落差和足够的水源,站址选择极其受限。废弃矿井则提供了一个现成的"地下空间"——矿道和巷道经过改造后可以作为地下水库,与地面水池或者塌陷区形成落差,小的落差也能跑低水头机组。这种做法把废弃矿山的空间资源利用起来,算是储能领域一个很有潜力的应用方向。
1.2 系统组成与整体技术路线
我搭的仿真系统包含五个部分:风电场、光伏电站、电池储能、废弃矿井小型抽水蓄能,以及一个典型工业园区的日负荷曲线。调度采用日前计划模式,时间分辨率取1小时,一共24个时段。这个粒度对验证算法和跑通流程足够了,工程上如果要做到15分钟级别的滚动调度,逻辑是一样的,只是变量数量翻几倍。
整个技术路线可以拆成五个环节:数据准备(风速、辐照度、负荷的序列生成)→ 单元出力建模(风机功率曲线、光伏出力公式)→ 储能系统建模(电池SOC递推、抽蓄水库能量状态)→ 优化模型构建(目标函数加约束条件)→ Python求解与结果分析。这样拆的好处是每个环节可以独立调试,哪块报错立刻能定位到。
项目里我用的系统基本参数如下:
| 组成单元 | 额定容量/关键参数 | 说明 |
|---|---|---|
| 风电场 | 60 MW | 切入风速3 m/s,额定风速12 m/s,切出25 m/s |
| 光伏电站 | 40 MW | 标准工况功率,温度系数-0.004/℃ |
| 电池储能 | 20 MW / 40 MWh | SOC范围0.1~0.9,充放电效率0.95 |
| 废弃矿井抽蓄 | 15 MW / 120 MWh | 发电效率0.85,抽水效率0.9,日水量平衡 |
| 负荷 | 峰值约75 MW | 典型工业负荷,早晚双高峰 |
这套参数规模不算大,但足够说明调度逻辑。实际项目中所有数值都可以按真实场站的容量替换。
2. 风光出力建模与数据处理
2.1 风电出力模型与功率曲线拟合
风电建模的第一步是生成风速序列。风速在统计上一般服从威布尔分布,我习惯用numpy.random.weibull生成形状参数为2的基序列,再乘以尺度参数来模拟一天内风速的均值水平。如果要做更贴近实际的研究,可以通过AR(1)自回归模型给风速加上时序相关性,这样风速不会在相邻时段突变,看起来更真实。
风速序列生成后,通过功率曲线换算成风电出力。风机功率曲线可以简化成三段线性模型:风速低于切入风速不出力;切入风速到额定风速之间,出力随风速近似线性上升;额定风速到切出风速之间维持额定出力;超过切出风速,风机切出保护,出力归零。公式化就是:
P = 0(v < v_ci 或 v ≥ v_co) P = P_r × (v - v_ci) / (v_r - v_ci)(v_ci ≤ v < v_r) P = P_r(v_r ≤ v < v_co)
下面这段是我在模型里实际用的模拟代码:
import numpy as np np.random.seed(42) T = 24 # 24小时 # 模拟一天风速(威布尔分布 + 白噪声平滑) shape, scale = 2.0, 6.0 wind_speed = np.random.weibull(shape, size=T) * scale # 简单滑动平均让风速曲线更平滑 wind_speed = np.convolve(wind_speed, np.ones(3)/3, mode='same') def wind_power_curve(v, v_ci=3.0, v_r=12.0, v_co=25.0, p_r=60.0): p = np.zeros_like(v, dtype=float) for i in range(len(v)): if v[i] < v_ci or v[i] >= v_co: p[i] = 0.0 elif v[i] < v_r: p[i] = p_r * (v[i] - v_ci) / (v_r - v_ci) else: p[i] = p_r return p P_w_avail = wind_power_curve(wind_speed)这里要注意,功率曲线的线性段只是简化处理,真实风机厂商给的功率曲线是非线性的,精度要求高的时候可以用多项式插值或者直接从曲线表里查值。但对于调度层的模型来说,线性近似已经能反映风电出力的基本变化规律。
2.2 光伏出力模型与辐照度模拟
光伏出力比风电更好建模,核心影响因素就是辐照度和组件温度。标准测试条件(STC)下组件出力为 P_stc,实际出力按辐照度比例折算,再乘一个温度修正系数:
P_pv = P_stc × (G / G_stc) × [1 + k × (T_cell - T_stc)]
其中 G_stc 取1000 W/m²,T_stc 取25℃,k 是温度系数,一般取 -0.004/℃。组件温度可以用环境温度加辐照度影响的简化经验公式来估算,练手阶段直接用固定环境温度加一个温度波动序列就够。
辐照度曲线我习惯用"余弦包络 + 云遮挡噪声"的方式模拟:白天从早晨开始上升,中午最大,傍晚归零,然后叠加随机云层导致的短时波动。云遮挡在时序上一般持续20到60分钟,所以我会用高斯噪声做平滑,而不是完全的白噪声。
# 模拟辐照度:余弦日变化 + 云层噪声 t = np.arange(T) hour = (t + 6) % 24 # 假设6点日出 G_max = 800.0 G = np.array([G_max * np.sin(np.pi * (h - 6) / 12) if 6 <= h < 18 else 0.0 for h in hour]) noise = np.random.normal(0, 60, size=T) noise = np.convolve(noise, np.ones(3)/3, mode='same') G = np.clip(G + noise, 0, 1000) P_stc = 40.0 T_cell = 28.0 + 0.03 * G # 组件温度近似 P_pv_avail = P_stc * (G / 1000.0) * (1 + (-0.004) * (T_cell - 25.0)) P_pv_avail = np.clip(P_pv_avail, 0, P_stc)这段代码做出来的光伏出力曲线带有明显的中午高峰和随机波动,比纯正弦曲线真实不少。需要注意,光伏模型里温度修正的系数是负数,高温会让组件出力下降,夏天中午反而会出现"温度型拐点",这个细节在调度分析里很容易被忽略。
2.3 数据清洗与典型日场景生成
模拟数据相对干净,但如果你用的是真实场站的历史数据,清洗这步就不能省。我的做法是三步走:先做物理阈值截断,风速超过40 m/s或者辐照度超过1100 W/m²的异常点直接剔除或限制;再做滑动窗口平滑,消除遥测设备造成的毛刺;最后做时间一致性检查,相邻时段出力突变幅度超过设定阈值(比如额定功率的50%)的,视为坏数据并插值替换。
如果手里有的一年数据都要用来做日前调度,计算量会很大。在前期验证阶段,可以用K-means对日出力曲线聚类,挑出几个典型日场景来代表"晴朗大风""阴天小风""多云"这类典型工况。聚类特征就用24点出力向量,距离度量用欧氏距离,聚类数选3到5个就够。用sklearn的KMeans就能实现,几行代码的事情。
3. 储能系统建模:电池与废弃矿井抽水蓄能
3.1 电池储能(BESS)建模要点
电池储能的建模核心是荷电状态(SOC)的递推关系。每个时段充进去的电量经过充电效率折算后增加SOC,放出来的电量要除以放电效率才是实际消耗的SOC:
SOC_t = SOC_{t-1} + η_ch × P_ch,t × Δt / E_rated - P_dis,t × Δt / (η_dis × E_rated)
实际建模时有几个容易被坑的细节。第一,SOC不能设成0到1的完整区间,锂电通常限制在0.1~0.9之间,这既是为了保护电池,也是为了防止模型在边界处出现退化解。第二,充放电功率上限和容量之间要匹配,比如20 MW的功率配40 MWh的容量,满功率充电2小时才能充满,这就决定了这套储能主要适合做短时调节而不是长时搬移。第三,同一时段同时充放电在物理上是禁止的,但线性规划如果不用0-1变量,很可能会出现"边充边放"的无意义解,解决办法是在目标函数里给充放电都加上一个很小的运行成本系数,模型就会自动避免这种浪费行为。
电池寿命衰减是个复杂问题。短期调度的仿真里可以不建寿命模型,但如果要做长时间尺度评估,建议加一个简化约束:每天累计充放电量不超过额定循环次数对应的能量吞吐,比如一天最多等效0.8次满充满放。
3.2 废弃矿井小型抽水蓄能建模
废弃矿井抽水蓄能的原理不复杂:利用地下巷道或采空区作为下水库,地表水池或塌陷区作为上水库,竖井当作压力管道。抽水时把水从巷道抽到地面,把风光过剩的电能转化为水的势能;发电时水从地面经竖井流回巷道,推动水轮机发电。
储能容量的理论估算公式是:
E = ρ × g × H × V × η_round
ρ取1000 kg/m³,g取9.8 N/kg,H是上下库的高差,V是有效储水容积,η_round是循环效率。举个例子,如果废弃矿井深度150米,巷道有效容积4万立方米,循环效率0.75,理论储能大约是1000×9.8×150×40000×0.75 = 4.41×10^9 J,换算成电量就是1225 kWh左右。如果巷道容积更大或者用多水平巷道并联,容量还能进一步提升。
但有一类容量约束特别重要:抽蓄并不是"想充多少就充多少",下库(巷道)的可用容积决定了一次循环能存多少水,同时为了防止巷道完全排空或者漫顶,水位需要保持在安全上下限之间。所以在调度模型里,抽蓄不能简单按功率上下限来约束,还要引入"能量状态"的递推和容量边界。
我在建模时采用了等效能量水库的方法,把水量直接折算成水库储能状态E_res(单位MWh):
E_res,t = E_res,t-1 + η_pump × P_pump,t × Δt - P_turb,t × Δt / η_turb
这个公式和电池SOC递推在形式上完全一致,只是把充放电效率的挂载位置对调了一下:充电(抽水)要乘效率,放电(发电)要除效率,这是因为抽水消耗的是电网电能,发电输出的是电能量,两者方向相反。这种统一形式的好处是,之后在优化求解部分可以把电池和抽蓄放在同一套框架里处理。
对于日调度,还要加一个关键约束:调度周期结束时,水库能量状态要回到初始值。如果不加这个约束,线性规划会"贪心"地把最后一小时的蓄水全部放空来满足功率平衡,结果看似完美,实际上第二天就没水可用了。
3.3 两种储能特性对比与协同价值
| 特性 | 电池储能 | 废弃矿井小型抽蓄 |
|---|---|---|
| 响应速度 | 毫秒~秒级 | 分钟级 |
| 循环效率 | 85%~95% | 70%~80% |
| 功率/能量密度 | 高 | 低,但容量可做大 |
| 循环寿命 | 数千次后衰减 | 数十年,无显著衰减 |
| 站址约束 | 无特殊要求 | 依赖废弃矿井存在 |
| 单位储能成本 | 高 | 低于电池(改造成本摊薄后) |
| 适用场景 | 调频、短时削峰、平抑快速波动 | 日循环调峰、长时能量搬移 |
两种储能放在同一个调度系统里,天然形成互补:电池负责"冲锋",快速响应光伏云层遮挡这种分钟级波动;抽蓄负责"长跑",承担小时级的能量搬移和日调峰任务。在优化模型里,由于两种储能的技术参数差异巨大,求解器会自然地分配角色——成本低的抽蓄承担时间跨度长的能量转移,电池则承担高功率短时调节。这也是多元储能协调的价值所在。
4. 优化调度模型构建
4.1 目标函数设计
这个项目用了单目标加惩罚项的处理方式,把弃风弃光、供电缺口、储能运行成本都折算成综合运行成本。目标函数写成:
min Σ_t ( λ_curt × CUR_t + λ_shed × SHED_t + λ_bat × (P_ch,t + P_dis,t) + λ_pump × (P_pump,t + P_turb,t) )
各项的含义和取值逻辑是:CUR_t 是第t时段的弃风弃光电量,取单位惩罚成本0.3元/kWh,让模型尽量消纳可再生能源;SHED_t 是供电缺额,惩罚成本取10元/kWh,比弃电惩罚高一个量级,保证"可靠性优先于经济性";电池和抽蓄的运行成本系数取0.005元/kWh,只是为了防止同时充放电的退化解,不代表真实度电成本。
这个目标函数的本质是"尽量少弃电、尽量不缺电、储能别做无用功"。如果你研究的场景是并网友好型而不是孤网,也可以把目标改成最大化可再生能源渗透率或者最小化联络线功率波动,但惩罚系数的相对大小关系要不变——缺电的代价始终应该是最大的。
4.2 约束条件梳理
约束条件是整个模型最需要耐心的部分,我梳理下来一共六类:
功率平衡约束是最基础的等式,每个时段都必须满足: P_w,t + P_pv,t + P_dis,t + P_turb,t + SHED_t = Load_t + P_ch,t + P_pump,t
这个式子的物理含义很清晰:电源侧出力加上缺电松弛变量,等于负荷加上储能吸收的功率。左边是"供给",右边是"需求",缺口用 SHED 来松弛,这样约束不会因为数据不合理而直接不可行。
然后是各类设备的技术约束。风电和光伏上网功率不能超过可用出力;电池充放电功率有上限;抽蓄发电和抽水功率有上限;电池SOC在0.1到0.9之间;抽蓄能量状态E_res在最小安全储量和最大库容之间。这些约束在LP里都是一堆上下界,写起来累,但逻辑直接。
最后是动态递推约束,也就是上一节里写的电池SOC递推方程和抽蓄能量递推方程,加上日末状态等于日初状态的水量平衡约束。这组约束是模型的核心,少了它们,储能就成了"空中楼阁"。
4.3 求解方法选择
这个模型是典型的线性规划问题,决策变量是各时段的功率值,目标函数和约束全部线性。我用的是SciPy的linprog,method选highs,它在稀疏矩阵处理上比默认的内点法更适合这种中大规模的调度问题。
如果你的模型需要考虑"抽蓄机组启停次数""电池充放电状态切换"这类逻辑,就需要引入0-1变量,问题会从LP变成混合整数线性规划(MILP)。这时候scipy的linprog就不够用了,建议上Pyomo加开源的CBC求解器,或者直接用商业求解器比如Gurobi的学术版。我个人在练手阶段用scipy就够了,因为纯LP模型已经把核心调度逻辑跑通了,0-1变量只是让模型更精细。
当然也可以用遗传算法、粒子群这类启发式算法来解,但我不推荐把它们作为唯一求解手段。启发式算法对非线性、非凸问题有效,用在这个线性模型上既慢又不稳定,同样的代码跑两次结果可能不一样。把它们留作和线性规划结果的对比验证,或者处理非线性约束时再用,才是正确姿势。
5. Python代码实现核心环节
5.1 项目目录结构与环境依赖
我的项目文件组织成下面这样,每个模块职责单一,后期改参数、换数据都不用翻整个工程:
wind_pv_storage_scheduling/ ├── data/ │ ├── wind_speed.npy │ ├── irradiance.npy │ └── load.npy ├── src/ │ ├── models.py # 风机、光伏、储能模型 │ ├── optimization_lp.py # 线性规划调度求解 │ ├── rule_based.py # 规则基准调度 │ └── plot_results.py # 结果可视化 ├── main.py └── requirements.txt依赖库就四个:numpy、pandas、matplotlib、scipy。pandas主要用于处理从CSV读取的风速、辐照度、负荷历史数据,数据量不大时也可以只用numpy。环境安装直接一行命令:
pip install numpy pandas matplotlib scipy如果你用的是Anaconda,numpy和pandas大概率已经装好了,补一个scipy和matplotlib就行。Python版本建议3.9或以上,linprog的highs求解器在老版本里可能不可用。
5.2 数据模拟与单元模型代码
主程序里先准备好一天的负荷序列和风光可用出力。负荷我模拟的是典型工业园区曲线,早上8点和下午17点各有一个高峰,夜间基础负荷在40 MW左右:
# 负荷序列生成 base_load = 45.0 load = base_load + 12.0 * np.exp(-((hour - 8) / 3) ** 2) + 15.0 * np.exp(-((hour - 17) / 4) ** 2) load += np.random.normal(0, 2.0, size=T)然后将第二节生成的风光出力序列和这里的负荷合并成一个输入数据集。到这里,模型的前半部分(电源出力和负荷需求)就准备好了。
5.3 基于线性规划的调度求解代码
这是整个项目的核心。我用一个8变量每时段的线性规划来实现:风电上网、光伏上网、电池充电、电池放电、抽蓄抽水、抽蓄发电、缺电、弃电。变量拼接方式是把不同属性的变量按时间顺序排成一个向量,索引计算需要特别小心,否则拼错一个位置就会得出完全错误的调度结果。
from scipy.optimize import linprog T = 24 # 定义变量索引,每个时段8个变量 # 0:PW 1:PV 2:CH 3:DIS 4:PM 5:TU 6:SHED 7:CUR n_vars = T * 8 # 目标函数系数 c = np.zeros(n_vars) for t in range(T): base = t * 8 c[base + 2] = 0.005 # 电池充电成本 c[base + 3] = 0.005 # 电池放电成本 c[base + 4] = 0.005 # 抽水成本 c[base + 5] = 0.005 # 发电成本 c[base + 6] = 10.0 # 缺电惩罚 c[base + 7] = 0.3 # 弃电惩罚 # 不等式约束:A_ub @ x <= b_ub A_ub = [] b_ub = [] for t in range(T): base = t * 8 # 风电/光伏上网功率不超过可用出力 row = np.zeros(n_vars) row[base] = 1 A_ub.append(row); b_ub.append(P_w_avail[t]) row = np.zeros(n_vars) row[base + 1] = 1 A_ub.append(row); b_ub.append(P_pv_avail[t]) # 电池充放电功率上限 row = np.zeros(n_vars) row[base + 2] = 1 A_ub.append(row); b_ub.append(20.0) row = np.zeros(n_vars) row[base + 3] = 1 A_ub.append(row); b_ub.append(20.0) # 抽蓄抽水/发电功率上限 row = np.zeros(n_vars) row[base + 4] = 1 A_ub.append(row); b_ub.append(15.0) row = np.zeros(n_vars) row[base + 5] = 1 A_ub.append(row); b_ub.append(15.0) # 缺电和弃电不能超过合理范围 row = np.zeros(n_vars) row[base + 6] = 1 A_ub.append(row); b_ub.append(load[t]) row = np.zeros(n_vars) row[base + 7] = 1 A_ub.append(row); b_ub.append(P_w_avail[t] + P_pv_avail[t]) # 等式约束:A_eq @ x = b_eq A_eq = [] b_eq = [] # 功率平衡约束 for t in range(T): base = t * 8 row = np.zeros(n_vars) row[base] = 1 # PW row[base + 1] = 1 # PV row[base + 3] = 1 # DIS row[base + 5] = 1 # TU row[base + 6] = 1 # SHED row[base + 2] = -1 # CH row[base + 4] = -1 # PM A_eq.append(row) b_eq.append(load[t]) # 电池SOC递推等式:SOC_t = SOC_{t-1} + 0.95*CH_t - DIS_t/0.95 soc = np.zeros((T, 1)) E_bat = 40.0 soc_init = 0.5 for t in range(T): base = t * 8 row = np.zeros(n_vars) row[base + 2] = -0.95 / E_bat # 充电使SOC增加,移到等式左边 row[base + 3] = 1.0 / (0.95 * E_bat) # 放电使SOC减少 if t == 0: A_eq.append(row) b_eq.append(soc_init) else: prev_base = (t - 1) * 8 row[prev_base + 2] = 0.95 / E_bat row[prev_base + 3] = -1.0 / (0.95 * E_bat) # 上面两行难以直接表示为A @ x,下面用逐时段拼接方式更清晰这里我遇到过一个典型问题:SOC递推是"当前时段状态等于上一时段状态加充放电影响",严格来说它应该写成:
SOC_t - 0.95/E_bat × CH_t + 1/(0.95×E_bat) × DIS_t - SOC_{t-1} = 0
但SOC并不是LP的决策变量,而是通过递推约束隐含定义的。更干净的做法是把每个时段的SOC也设为决策变量,然后约束联结。我在实际项目里的处理方式是给每个时段同时加入SOC变量(第8个之后的扩展变量),然后写递推等式。为了文章篇幅,这里给出的是简化版本,核心逻辑已经能说明白——真正在本地跑的时候,建议把所有变量(包括SOC和水库能量状态)统一作决策变量,这样约束搭建会更直观、也更好排查问题。
最后求解并解析结果:
res = linprog(c, A_ub=np.array(A_ub), b_ub=np.array(b_ub), A_eq=np.array(A_eq), b_eq=np.array(b_eq), bounds=[(0, None)] * n_vars, method='highs') if res.success: x = res.x P_w = x[0::8] P_pv = x[1::8] P_ch = x[2::8] P_dis = x[3::8] P_pm = x[4::8] P_tu = x[5::8] shed = x[6::8] curt = x[7::8]说实话,linprog跑这个规模的问题完全没压力,求解耗时在毫秒级。真正费时间的反而是矩阵搭建过程,索引一不留神就会错位,我在调试阶段用了一个土办法:先求出解后手动验证几个典型时段的结果,比如夜间光伏为零时,PV变量必须严格为0,否则说明索引拼错了。
5.4 调度结果可视化与指标评估
调度结果光看数字不够直观,我习惯画一张堆叠图把一天24小时的功率分配展示出来:横轴是时间,纵轴是功率,风电、光伏、电池放电、抽蓄发电依次堆叠,负荷用折线叠加在图上。电池SOC和抽蓄能量状态单独画在下方子图。下面是可视化代码的核心部分:
import matplotlib.pyplot as plt fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True) ax = axes[0] ax.stackplot(range(T), P_w, P_pv, P_dis, P_tu, labels=['Wind', 'PV', 'Battery Disch.', 'PHS Gen.'], alpha=0.7) ax.plot(range(T), load, label='Load', color='black', linewidth=2) ax.plot(range(T), P_ch + P_pm, label='Charging/Pumping', linestyle='--', color='red') ax.set_ylabel('Power (MW)') ax.legend(loc='upper left') ax.set_title('Optimal Dispatch Result') ax = axes[1] ax.plot(range(T), soc, label='Battery SOC', marker='o') ax2 = ax.twinx() ax2.plot(range(T), e_res, label='PHS Energy State', color='green', marker='s') ax.set_ylabel('SOC') ax2.set_ylabel('E_res (MWh)') ax.legend(loc='upper left') ax2.legend(loc='upper right') plt.tight_layout() plt.show()画完图先别急着下结论,对着图看一眼这几个"应该出现"的特征:光伏出力高的中午时段,电池和抽蓄应该在充电;下午负荷高峰时段,抽蓄或者电池应该在放电;夜间如果风大且负荷低,应该出现弃电或者抽蓄充电,而不是强行让风电场降到很低的出力水平。
评估指标方面,我最关注三个数字:可再生能源利用率 = (总可用风光 - 弃电量) / 总可用风光;供电可靠性用失负荷率衡量,即缺电总量除以负荷总量;储能利用情况看两个储能单元的日循环次数和总吞吐量。这几个指标算完了,项目的结论基本也就成型了。
6. 常见问题与排查技巧实录
6.1 高频疑难与解决方案速查表
调试这个项目的过程中,我前后遇到过不少问题,挑几个高频的列成速查表,都是实测过的解决方案:
| 常见问题 | 可能原因 | 排查与解决办法 |
|---|---|---|
| 求解结果提示不可行 | 等式或不等式约束之间存在矛盾 | 给功率平衡等式加缺电松弛变量SHED,先跑通再加严格约束 |
| 电池出现同时充放电 | 目标函数里没有充放电成本项 | 给充放电变量加极小的正系数,比如0.005 |
| 抽蓄日末能量归零 | 缺少水量平衡约束 | 强制E_res日末等于日初,可以放宽到不小于初始值 |
| SOC曲线一直贴在下限 | 电池容量或功率参数设置不合理 | 检查E_rated与P_max的匹配关系,看是否2小时就充满/放完 |
| 弃风弃光量异常高 | 消纳能力不足或储能容量过小 | 看储能是否在风光高峰时段满充,考虑增大抽蓄容量 |
| 结果对初始SOC极其敏感 | 没有让SOC有恢复空间 | 初始SOC设0.5,并观察日末是否回到合理区间 |
这里特别想提醒的是,不可行问题百分之八十是因为约束太紧而不自知。比如你同时要求"功率平衡严格相等"和"电池容量很小",在负荷高峰又强制风电全额上网,那肯定无解。遇到这种情况,先加松弛变量把模型跑通,再逐步收紧,比盯着报错信息干想高效得多。
6.2 踩坑经验与调参心得
有几个经验是做了三四个版本之后才总结出来的。
第一,时间分辨率真的会影响结论。我用1小时粒度算出来的抽蓄调度,是一个时段要么抽水要么发电的"方波"型模式;改成15分钟粒度后,因为风光出力波动更精细,抽蓄的出水曲线就平滑很多,也更接近真实运行。所以我建议前期验证用1小时粒度,写论文或者出工程结论时尽量跑15分钟粒度。
第二,目标函数的量纲一定要统一。我一开始把所有成本都写成了万元,结果惩罚系数设成0.0001,求解器直接把这些项忽略了,导致模型疯狂弃电。后来把所有单位统一成元/kWh或者元/MWh,量纲一致后再调整惩罚系数之间的比例关系(缺电>弃电>运行损耗),模型行为才正常。
第三,废弃矿井抽蓄的水头变化不能完全忽略。我最早用的固定水头模型,把E_res当作线性能量库,算出来的结果偏乐观。因为巷道水位变化会影响实际发电水头,进而影响出力功率。后来我在模型里加了水头修正系数,发电效率随能量状态线性变化,虽然多了非线性约束,但结果更可信。如果不想引入非线性,可以按平均水头打折处理,至少留个安全裕度。
另外,对于想把这个项目往论文方向扩展的朋友,有几个方向值得考虑:加入风电功率预测和光伏功率预测的不确定性(用场景法或者鲁棒优化处理);在目标函数中引入碳排放成本来体现绿色价值;把调度周期从日前扩展到"日前+日内滚动"的两阶段框架,让模型更贴近实际运行。这些扩展在代码层面不需要推翻重来,只要在现有LP框架上增加变量和约束就能实现,这也是用线性规划建模的优势所在。
最后再分享一个我自己的习惯:每次跑完调度,我第一件事不是看目标函数值,而是先把SOC和E_res曲线拉出来看一遍。如果这两个储能状态曲线没有"有涨有落"的合理循环,说明模型大概率在"偷懒"——要么在借松弛变量逃避约束,要么根本没在合理利用储能。模型可以复杂,但物理直觉永远是最先信得过的那个判断标准。