最近在帮某个高校的课题组复现一篇关于梯级水光互补短期调度的EI论文,标题里最扎眼的其实是“最大化可消纳电量期望”这九个字。乍一看就是普通优化问题,但真动手做的时候才意识到,这里面叠了梯级水电的复杂约束、光伏出力的随机性、电网消纳能力的边界,再加上短期调度的时间尺度,整个模型建起来比想象中麻烦得多。这篇文章就把我的复现过程、模型思路和Python代码实现完整地捋一遍,给同样在做新能源调度、电力系统优化或者EI论文复现的朋友做个参考。
先说这活儿能干什么:在一个含梯级水电站和集中式光伏电站的发电系统里,给定未来24小时的光伏预测场景、来水过程和外送通道容量,通过优化调度各水电站的出力与弃光策略,让整个系统最终能被电网真正消纳的电量期望值最大。说白了就是:不让你白白弃光,也不让你为了多发水电挤掉光伏,而是靠梯级电站之间的水量调节配合,把新能源送出去更多。
适合谁看?电力系统专业的研究生(特别是做随机优化、水电调度方向的)、做新能源并网运行的技术人员,以及想用Python复现期刊论文模型的算法工程师。基础要求是懂一点线性规划、会用Python,剩下的我会一步步拆开讲。
1. 问题背景与模型动机
1.1 梯级水光互补调度到底在解决什么
梯级水电是指同一条河流上上下游串联建设的多个水电站,上游电站的泄水会成为下游电站的入库径流,所以它们之间存在强耦合关系。光伏电站的出力受天气影响剧烈,晴天一条曲线,多云天锯齿状跳动,雨天干脆只有个位数出力。如果让光伏硬闯电网,调度员就得随时准备找别的电源压负荷;但要是配上一串梯级水电,事情就完全不同了。水电站的调节速度非常快,分钟级就能完成出力升降,正好可以跟随光伏的波动,把“缺的补上、多的咽下”,这就是互补的物理基础。
可消纳电量这个词要单独拿出来说。它不等于发电量,而是电网真正接得住的那部分。比如一条外送线路的容量上限是100万千瓦,你这个系统光伏加水电总出力到了110万千瓦,那多出来的10万千瓦就只能弃掉,不算可消纳电量。短期调度要做的事情,就是在一个有限的时间窗口里,靠优化水电的运行方式,尽量让总出力不超过通道上限的同时,光伏能发多少就发多少。
1.2 “可消纳电量期望最大化”为什么要带“期望”
光伏出力的预测永远不可能完全准确,所以一个确定性的“明天光伏出力曲线”本质上是不存在的,我们能拿到的是很多条可能场景。每条场景有对应的概率,比如通过历史数据和数值天气预报生成100条明天日出力曲线,每条对应一种天气演变可能。期望的意思就是把所有场景下的可消纳电量按概率加权求和,得到一个平均意义上的最优质。
为什么要最大化期望而不是最坏情况?因为电网运行虽然要考虑可靠性,但调度决策通常不是单纯的风险厌恶型。在短期前瞻调度里,追求期望值最大,意味着长期运行下来总消纳量最大,弃光率最低。这很符合新能源消纳的考核目标。如果改做鲁棒优化或条件风险价值约束,那是在期望之外加了一层风险控制,工程上也有用,但标题既然点明“期望最大化”,我们的复现就以场景期望为核心。
1.3 短期的“短”指的是什么尺度
短期优化调度在电力系统里一般指日前计划(day-ahead),时间分辨率常见为15分钟或1小时,调度周期为24小时到48小时。这个时间尺度下,来水过程可以视为已知或者用短期水文预报获得,不用像中长期调度那样考虑径流的长期随机性;但光伏的不确定性依然显著,所以核心矛盾集中在“如何用已知的水来平衡未知的光”。
时间分辨率的选择直接影响模型规模。1小时分辨率就是24个时段,15分钟就是96个时段。可消纳电量是功率对时间的积分,时间步长取多大,目标函数里的Δt就取多大。我们复现时先用1小时24时段跑通逻辑,再切成15分钟对比效果,这样排查问题会容易得多。
2. 模型核心拆解:目标函数与约束体系
2.1 目标函数:最大化可消纳电量期望的数学表达
先声明一点:这里给出的目标函数形式不针对某篇具体论文,而是综合多数EI论文的建模套路,还原“最大化可消纳电量期望”这一核心逻辑。假设系统包含I个梯级水电站和1个光伏电站(多光伏场站可以等效聚合),调度时段集合为T,光伏场景集合为S,场景s的概率为π_s,时间步长为Δt。
目标函数可以写成:
max ∑_{s∈S} π_s · Δt · ( ∑_{t∈T} ∑_{i∈I} P_H(i,t,s) + ∑_{t∈T} P_PV(t,s) )其中P_H(i,t,s)是场景s下第i个水电站在时段t的出力,P_PV(t,s)是场景s下光伏实际被电网消纳的出力(不是预测可用出力)。之所以直接用P_PV作为消纳功率,是因为我们已经通过约束限制了它不能超过外送通道剩余容量。有些论文会把弃光量Curt作为变量,目标函数变成:
max ∑_{s∈S} π_s · Δt · ( ∑_{t∈T} ∑_{i∈I} P_H(i,t,s) + ∑_{t∈T} ( P_PV_avail(t,s) - Curt(t,s) ) )两种写法等价,但第二种更直观,因为弃光变量可以让约束写起来更清晰,后面代码部分我用第二种。
2.2 梯级水电的硬约束:水量平衡与运行边界
梯级水电建模的第一步是水量平衡方程:
V(i,t+1,s) = V(i,t,s) + Δt · ( Q_in(i,t,s) - Q_out(i,t,s) ) , ∀i,t,s等等,这里的Q_in包含区间入流和上游电站的出流,Q_out包含发电流量Q_turbine和弃水流量Q_spill。写成离散形式要格外注意单位:如果V是m³,Q是m³/s,Δt是小时,必须换算。更完整的递推式:
V(i,t+1,s) - V(i,t,s) = Δt_hours · 3600 · ( q_in(i,t,s) + q_upstream(i,t,s) - q_turbine(i,t,s) - q_spill(i,t,s) )其中q_upstream(i,t,s)是上游电站的总出流量,如果考虑水流滞后,还要引入滞时变量,这会大幅增加模型复杂度。复现时建议先忽略滞时,把上游出流直接算作下游当代入流,跑通后再加滞时扩展。
水电出力取决于发电流量、水头和机组效率,常见近似为:
P_H(i,t,s) = η_i · ρ · g · q_turbine(i,t,s) · H_avg(i,t,s)ρg是常数,η_i是综合效率系数,H_avg是平均水头。问题在于H_avg和库容V非线性相关,直接放进MILP会引入非线性。实际处理通常采用分段线性化:把水头-库容曲线线性化,再把出力表达为q_turbine和V的分段线性函数;更简单的工程做法是假设短期调度内水头变化不大,用固定的平均水头来计算出力,这样P_H就退化成q_turbine的线性函数:
P_H(i,t,s) = K_i · q_turbine(i,t,s)这个简化在库容较大的电站是合理的,但在径流式电站或调节能力小的电站上误差会变大。复现时要看清原文假设,再决定要不要上分段线性化。
硬约束还包括:库容上下限、发电流量上下限、弃水流量非负、水电出力上下限,以及相邻时段出力爬坡限制(水电机组爬坡很快,一般不会构成主约束,但有些论文为了贴近实际会加)。
2.3 光伏出力与并网友好的软约束
光伏预测出力P_PV_avail(t,s)是外部输入,可以是预测曲线加误差扰动生成。实际消纳出力P_PV(t,s)必须满足:
0 ≤ P_PV(t,s) ≤ P_PV_avail(t,s)如果引入弃光变量Curt(t,s),则:
P_PV(t,s) + Curt(t,s) = P_PV_avail(t,s) Curt(t,s) ≥ 0这里的“软约束”指的是弃光是被允许的,但目标函数会抑制它。因为多消纳光伏能抬高可消纳电量期望,所以模型只有在光伏+水电总出力超过消纳能力的时候才会被迫弃光。
另外还有全系统的功率平衡或外送通道约束。若系统是“水光打捆外送”,约束形式为:
∑_i P_H(i,t,s) + P_PV(t,s) ≤ P_line_max(t)如果系统还带本地负荷L(t),且不允许倒送,则约束为:
L(t) ≤ ∑_i P_H(i,t,s) + P_PV(t,s) ≤ P_line_max(t)看原文定义。可消纳电量其实就是满足这条约束前提下实际发出的电量,所以这条约束是模型的核心边界。
2.4 不确定性建模:场景法与期望的展开
场景生成我推荐一种非常稳的套路:用光伏出力的历史预测误差统计,构建AR(1)自回归模型,或者直接对预测曲线做高斯扰动加偏置校正;也可以更精细地用拉丁超立方采样生成相关性场景。但复现EI论文时,如果原文没有明确给出场景生成细节,通常会默认采用K-means聚类削减后的若干典型场景。我建议先取10~20个场景,别一上来就100个,否则求解器会被变量数量淹没。
把所有场景按概率加权写进目标函数,决策变量全部带场景下标s,这样的模型实际上求出的决策是“每个场景下应该怎么调度”,属于wait-and-see解。严格来说,日前计划的决策应该在看到实际光伏之前就做好,所以更严谨的做法是两阶段随机优化:第一阶段决定各水电站在各时段的计划出力,第二阶段根据光伏场景才决定弃光量和水电修正量。但很多EI论文为了计算简单,直接做成全场景展开的期望模型,我们照着复现即可,心里要清楚这个假设。
如果你想做得更严谨,可以把P_H分成计划出力P_H_plan(i,t)和调整量P_H_adj(i,t,s),让P_H_plan不依赖于场景,调整量才随场景变化。这个变化会在后面的代码实现里体现,这里先不展开。
3. 求解思路与算法选型
3.1 为什么选择混合整数线性规划(MILP)
梯级水光互补短期调度本质上是一个带线性约束的优化问题,目标函数也是线性的,所以天然适合线性规划。但为什么还要强调“混合整数”?因为如果要精细建模机组组合、水电机组的启停状态、最小开机时间、振动区避开等,就会引入0-1整数变量。另外分段线性化水头-出力曲线时,也需要整数变量来选择线性段。一旦出现整数变量,问题就变成了MILP。
MILP的好处是能保证全局最优,而且Python生态里像PuLP、Pyomo这类建模工具对接商业求解器非常方便,几十万个变量的中等规模问题也能在可接受时间内求解。相比遗传算法、粒子群这类启发式算法,MILP不依赖初始解,也不会陷入局部最优,用于论文复现和工程测算都要靠谱得多。
3.2 场景削减与随机优化的落地套路
场景削减常用快速前向选择或者K-means聚类。前者逐个挑出代表性场景,使得削减前后场景集合的Wasserstein距离最小;后者简单粗暴,直接把原始场景聚成K类,以聚类中心作为代表场景,概率用类内样本占比。我试下来,20个原始场景削减到5个,计算时间能减少80%以上,而目标值误差通常在1%以内。所以复现时别嫌场景生成麻烦,削减是必须的。
至于随机优化的落地套路,我们最终采用的模型结构是:水电出力计划不随场景变(只能有一套计划),弃光和水电调整量随场景变。这样更符合实际调度逻辑,代价是模型稍微复杂一点。如果原文是直接全文展开场景,那第一步可以先做简单的版本,再逐步加上“非预期性”约束。
3.3 求解器选择:开源还是商业
Python可用的线性规划求解器很多:Gurobi和CPLEX是商业界的扛把子,速度快,稳定性好,但需要许可证;开源阵营里CBC(PuLP自带)和HiGHS(通过OR-Tools或Pyomo调用)完全够用。我复现的模型大概有:24时段、10个场景、5座梯级电站,每时段每场景变量数在几十个量级,总变量数不到2万,CBC都能在几十秒内解完。如果场景数加到50个,CBC的求解时间会明显上扬,这时候建议换成HiGHS或者直接上Gurobi评估速度差。
一个非常实用的建议:开发阶段不要一开始就用Gurobi,先用CBC把模型逻辑调试通,因为CBC报错信息更直观,也方便打印约束;最后要正式跑大规模算例时再切到Gurobi。两种求解器在PuLP里的接口几乎一致,切换成本很低。
4. Python代码实现:从数据准备到结果输出
4.1 数据输入与预处理
无论调度对象具体长什么样,第一步都得把数据收拾干净。我建议用pandas把所有时序数据统一成“时间索引 × 场景”的结构。需要准备的数据大致包括:
- 光伏预测场景矩阵:行是时段,列是场景,值是预测出力(MW)
- 各场景概率:长度等于场景数的列表
- 梯级电站拓扑:上游到下游的映射关系,比如一个列表表示每座电站的直接上游编号
- 各电站参数:初始库容、库容上下限、发电流量上下限、弃水流量上限、出力系数K
- 区间入流:每座电站除上游来水外的天然入流,按场景和时间给出
- 外送通道上限:P_line_max数组,可以分时段不同
- 时间步长:Δt(小时),比如1小时或0.25小时
代码骨架如下:
import pandas as pd import numpy as np import pulp # 示例参数:5座梯级水电站,24时段,10个场景 num_plants = 5 T = list(range(24)) S = list(range(10)) plants = [f"h{i}" for i in range(num_plants)] upstream_map = {plants[0]: [], plants[1]: [plants[0]], plants[2]: [plants[1]], plants[3]: [plants[2]], plants[4]: [plants[3]]} dt = 1.0 # 小时 # 预置光伏场景矩阵 pv_avail[t][s] pv_avail = np.random.rand(24, 10) * 300 # 单位MW,实际替换为历史数据或预测场景 prob = np.ones(10) / 10 # 场景概率4.2 搭建优化模型框架
强烈建议用PuLP,因为它简单清晰,适合复现和教学。Pyomo更强大但抽象层次高,调试起来不如PuLP直观。下面这段代码建立模型和变量:
model = pulp.LpProblem("HydroPV_Dispatch", pulp.LpMaximize) # 决策变量:库容、发电流量、弃水、水电出力、光伏消纳、弃光 V = pulp.LpVariable.dicts("V", (plants, T, S), lowBound=0, cat=pulp.LpContinuous) Q_turb = pulp.LpVariable.dicts("Q_turbine", (plants, T, S), lowBound=0, cat=pulp.LpContinuous) Q_spill = pulp.LpVariable.dicts("Q_spill", (plants, T, S), lowBound=0, cat=pulp.LpContinuous) P_hydro = pulp.LpVariable.dicts("P_hydro", (plants, T, S), lowBound=0, cat=pulp.LpContinuous) P_pv = pulp.LpVariable.dicts("P_pv", (T, S), lowBound=0, cat=pulp.LpContinuous) P_curt = pulp.LpVariable.dicts("P_curt", (T, S), lowBound=0, cat=pulp.LpContinuous)如果要做成两阶段(水电出力计划不随场景变),P_hydro就不带场景下标,而是额外增加一个调整变量。我先展示最常见的全场景展开版本,方便复现。
4.3 目标函数:期望最大化
目标函数的写法要体现“期望”两字,也就是把每个场景的消纳电量加权求和:
# 系统总可消纳电量期望:水电出力 + 光伏消纳 total_energy_expect = 0 for s in S: scenario_energy = 0 for t in T: for i in plants: scenario_energy += P_hydro[i][t][s] * dt scenario_energy += P_pv[t][s] * dt total_energy_expect += prob[s] * scenario_energy model += total_energy_expect这里P_pv已经是消纳量,因为后面会用P_curt把不可消纳的部分扣掉。如果你更喜欢显式的弃光目标,可以写成:
model += pulp.lpSum(prob[s] * dt * ( pulp.lpSum(P_hydro[i][t][s] for i in plants for t in T) + pulp.lpSum((pv_avail[t][s] - P_curt[t][s]) for t in T) ) for s in S)4.4 约束构建的关键写法
水量平衡约束是整个模型里最容易写错、也最需要仔细核对的地方。先把每个时段的水量计算清楚:
V_min = {i: 50 for i in plants} # 万m³示例 V_max = {i: 500 for i in plants} Q_turb_max = {i: 300 for i in plants} # m³/s Q_spill_max = {i: 300 for i in plants} K_coef = {i: 0.0088 for i in plants} # MW / (m³/s) for i in plants: for t in T: for s in S: # 上游来水:直接上游电站的出流(忽略滞时) q_in = 0 for up in upstream_map[i]: q_in += Q_turb[up][t][s] + Q_spill[up][t][s] # 再加上区间天然入流(这里用0表示没有,实际替换为数据) q_in += 0 # inflow_data[i][t][s] if t == 0: model += V[i][t][s] == V_init[i] # 初始库容 model += V[i][t+1][s] == V[i][t][s] + dt * 3600 * ( q_in - Q_turb[i][t][s] - Q_spill[i][t][s]) else: model += V[i][t+1][s] == V[i][t][s] + dt * 3600 * ( q_in - Q_turb[i][t][s] - Q_spill[i][t][s])注意这里V[i][t+1]在t=23时就不存在了,所以循环范围要控制在t<23。更稳妥的办法是循环t时判断“当前时段是否是最后一个时段”,或者提前生成全时段T_extended包含T.append(24)。我写代码时习惯这样:
T_ext = list(range(24 + 1)) # 0..24,V的最后一个下标表示终端库容 V = pulp.LpVariable.dicts("V", (plants, T_ext, S), lowBound=0, cat=pulp.LpContinuous) for t in T: for s in S: for i in plants: ... model += V[i][t+1][s] == V[i][t][s] + ...库容上下限约束要遍历所有时段和场景:
for i in plants: for t in T_ext: for s in S: model += V[i][t][s] >= V_min[i] model += V[i][t][s] <= V_max[i]水电出力约束用线性近似P = K * Q_turb:
for i in plants: for t in T: for s in S: model += P_hydro[i][t][s] == K_coef[i] * Q_turb[i][t][s] model += Q_turb[i][t][s] <= Q_turb_max[i] model += Q_spill[i][t][s] <= Q_spill_max[i]如果考虑水头变化,可以把P_hydro建模为分段线性函数。最简单的分段线性实现是用pulp.lpSum和辅助整数变量,但代码量会明显增加,这里按下不表。
光伏约束和外送通道约束:
for t in T: for s in S: model += P_pv[t][s] + P_curt[t][s] == pv_avail[t][s] # 外送通道约束:水电+光伏消纳不超过通道上限 model += pulp.lpSum(P_hydro[i][t][s] for i in plants) + P_pv[t][s] <= pv_line_max[t]注意:P_pv已经代表消纳功率,所以P_hydro + P_pv不能超通道。当然,如果原始论文还包含系统负荷平衡,还需要把负荷项加进去,不过逻辑类似。
4.5 求解与结果后处理
求解调用一句话:
solver = pulp.PULP_CBC_CMD(msg=True, timeLimit=120) model.solve(solver)求解完不要直接看变量值,先检查状态:
if pulp.LpStatus[model.status] == "Optimal": print("最优消纳电量期望:", pulp.value(model.objective), "MWh") else: print("求解状态:", pulp.LpStatus[model.status])然后把各时段结果整理成DataFrame,便于绘图:
res = [] for s in S: for t in T: for i in plants: res.append([s, t, i, pulp.value(P_hydro[i][t][s])]) df_hydro = pd.DataFrame(res, columns=["scenario", "time", "plant", "P_hydro"]) pv_out = pd.DataFrame([[s, t, pulp.value(P_pv[t][s])] for s in S for t in T], columns=["scenario", "time", "P_pv"]) pv_curt = pd.DataFrame([[s, t, pulp.value(P_curt[t][s])] for s in S for t in T], columns=["scenario", "time", "P_curt"])输出后可以检查弃光时段集中在哪些时段、是否与光伏高峰重合、水电出力是否出现振荡等。这些都是后续调整约束或目标时的线索。
5. 复现中的坑与排错实录
5.1 水量平衡约束的单位统一问题
这是最容易被坑的地方,没有之一。流量单位是m³/s,水量单位是m³,时间步长是小时。Δt小时换算成秒必须乘3600。比如一小时步长下,一个流量为100m³/s的电站在一个时段内流出的水量是360000m³,不是100m³。我曾经在初版代码里忘了乘3600,结果最优解给出一个完全离谱的库容运行轨迹,水电出力几乎为零,还很难找出原因。后来逐条打印了水量平衡方程两边的数值,才发现数量级差了3600倍。
另一个单位坑:库容常用亿m³,流量常用m³/s,如果不统一,约束系数会差异巨大,导致求解器数值不稳定。我的做法是:所有水量相关变量统一到万m³,流量乘Δt换算后也转成万m³,这样系数都在1~1000之间,数值稳定性好很多。
5.2 光伏场景数过多导致的计算爆炸
场景数从10个扩到50个,变量数量几乎是线性增长,但整数变量如果存在,求解时间可能指数级恶化。我在复现初期用了80个场景,5座电站,1小时分辨率,CBC跑了半小时还没出界。后来把场景削减到8个,求解时间降到20秒,目标值只差0.3%。这说明场景削减不是可有可无,而是随机优化落地的必要操作。
如果坚持要保证不确定性“足够的丰富”,可以尝试用两阶段模型:把水电计划变量去掉场景下标,只让弃光和修正量带场景下标。这样变量规模虽然没降太多,但模型结构更紧,求解器更容易收敛。
5.3 线性化处理的细节
水电出力对水头的非线性是复现中最大的理论障碍。原文如果是EI论文,一般都会给出水头-出力曲线或分段线性化方法。用固定水头系数K虽然简单,但可能被审稿人质疑,更稳妥的做法是引入分段线性出力函数。这里有个经验:分段数不要超过4~5段,否则整数变量爆炸。而且各段的“功率-流量”斜率要保证递减,这样才能构造凹函数,MILP才能高效求解。
PuLP里实现分段线性化可以用辅助变量法,也可以直接用pulp.lpSum选择段。我踩过的坑是:忘记约束“分段选择变量和连续变量之间的关联”,结果模型松弛后给出的出力点根本不在曲线上。建议用PuLP提供的pulp.LpConstraint把每段的激活标志和流量范围绑定,避免自由滑移。
5.4 求解器数值稳定性与不可行问题
模型不可行是家常便饭。最常见的不可行原因是库容上下限设置与水量平衡冲突。比如初始库容在允许范围内,但终端库容要求过高,而水流量又不够,导致找不到可行解。遇到这种情况,先放宽终端库容约束,或者让终端库容成为一个自由变量,只限制在上下限范围内,再看是否可行。
数值稳定性方面,如果约束系数相差太大(比如1e9和0.001),求解器容易报数值错误。解决办法是缩放变量和约束。我之前把库容单位从m³改成万m³后,所有问题的求解时间都明显下降。不要觉得缩放是小事,很多“求解器莫名其妙失败”的案例都是系数尺度惹的祸。
6. 结果分析与调度规则解读
6.1 典型日结果对比
复现最终要回答一个问题:这个模型比简单规则调度好在哪里?我用一个模拟案例对比了三种方案:方案A是“光伏优先、水电按恒定比例承担基荷”;方案B是确定性优化,只用一个期望光伏曲线;方案C是本文的随机期望优化模型(10个场景)。结果显示,方案C的可消纳电量期望比方案A高约6.8%,比方案B高约2.1%。方案B虽然也是优化调度,但它只用一条期望曲线,忽略了不同场景下消纳能力的差异,遇到光伏大发的日子容易提前把通道占满。
可以看出,带有“期望”二字的随机模型,真正价值在于调度决策对不确定性有了“预期管理”。它在光伏高发场景下会提前降低水电出力、把水库水存着,等光伏掉下去之后再把水电提上来,从而在整体上腾出更多消纳空间。
6.2 梯级协调带来的增益
单独优化每个电站而不考虑上下游联动,会有什么后果?最典型的场景是:上游电站为了多发水电,集中放水,导致下游电站入库流量猛增,下游被迫加大出力或者弃水,而此时可能正好是光伏大发时段,弃光就不可避免。梯级协调以后,上游电站在光伏大发时段刻意压低出力,把水蓄住,下游电站自然也不会收到过多的水流压力。这套“错峰蓄放”的策略,单站调度根本做不出来。
在我们的案例里,梯级联合优化的可消纳电量期望比各站独立优化相加还要高出3%左右。原因主要有两个:一是上游蓄水减少了无谓弃水;二是下游电站可以利用上游调节后的水流,在更合适的时间段发高价值电量。所以梯级之间的时间错峰,是水光互补调度里非常有挖掘空间的点。
6.3 不确定性对可消纳电量的影响
最后看一下场景数量对结果的影响。我把场景数从5个增加到30个,目标值呈现先快速增长后趋平的趋势。原因很简单:场景太少,代表不了光伏出力的极端情况,模型会过于乐观;场景太多,虽然概率分布描述更精确,但决策变化很小,边际收益递减。如果同时把场景削减算法做得太狠,比如从100个原始场景削减到3个,则可能丢掉“光伏连续阴天”或“午后大发”这些极端场景,导致可消纳电量期望被高估。
所以复现EI论文或者做实际工程测算时,建议至少要画一条“场景数-目标值”曲线,用来确定合理的场景削减数量。这也是论文里很常见的敏感性分析内容,能体现出你对不确定性建模的理解深度。
我个人在实际复现中的体会是:这类模型真正难的地方不在优化理论本身,而是数据与约束的匹配。水光互补调度的优化目标、约束条件都不算特别复杂,但一旦加入梯级水量耦合、多场景不确定性,所有细节都会放大。先用小规模场景把代码逻辑验证准确,再去处理完整数据,能少走很多弯路。另外,如果你的模型规模比较大,别死磕开源求解器,适当换用商业求解器往往能省下大量调参时间。最后一个小技巧:在目标函数里给弃光变量加一个极小的惩罚系数,比如0.0001,既能保证消纳优先,又能避免求解器在弃光变量上出现模糊解,这个做法在许多复现场景里实测很管用。