风光储项目做规划,最让人头疼的不是风机和光伏板的选型,也不是储能到底配几小时的容量,而是“风大光好的时候怎么办、晚上没风的时候又怎么办”。我见过很多团队一开始拍脑袋定了一个配比,等运行数据回来发现,储能整天满充满放,循环寿命掉得飞快,弃风弃光倒是少了,可经济账全算不过来了。说白了,风光储规划本质上是一道“在不确定性里做决策”的题目。
鲁棒优化这几年被频繁提起,很多人把它当成随机规划的替代品,觉得只要建一个“最坏情况”模型就能把问题解决。实际做下来会发现,事情没那么简单——尤其是多阶段规划,投资决策和运行决策交织在一起,不确定性跨时段传导,一个阶段拍板错了,后面怎么调都很难补回来。但恰恰是这种“先定投资、后做运行,边看边改”的结构,和鲁棒优化的两阶段或三阶段模型天然契合。这篇文章就聊聊我实际搭建风光储多阶段规划模型时的一些思路和踩过的坑,不讲虚的,全是能直接抄作业的干货。
1. 问题到底难在哪:风光储规划的底层逻辑
先把这个问题的复杂度拆开来看。一个完整的风光储联合系统规划,通常要回答三个问题:建多大、建在哪、怎么调度。建多大是容量配置,风机、光伏、储能各自的装机规模和能量容量;建在哪是选址问题,涉及可用土地、接入点距离、地形约束;怎么调度则是运行层面的问题——在给定装机下,每个时刻的充放电功率、弃电策略、调峰补偿该怎么定。
这三个问题不是独立的。容量配置决定运行方式的上限,运行结果又反过来影响容量配置的经济性。如果你先定容量再算运行,那运行成本的误差会被容量决策放大;如果你把所有时段、所有场景全部联合优化,模型规模又会爆炸,求解器直接跑不动。这也是风光储规划区别于单一电源规划的核心难点——它在时间尺度上是嵌套的,在不确定性维度上是耦合的。
再说不确定性。风光出力不是一组固定的序列,它有日出规律、季节规律,但每天都不一样。风速差一点,整个出力曲线就变形;云层飘过来,光伏五分钟内可以掉一半出力。随机规划的做法是抽一批典型场景,每个场景带概率,然后求期望最优;鲁棒优化的做法则完全不同,它不关心概率,只关心“如果坏情况真的发生了,我的方案还扛不扛得住”。
这时候就涉及一个关键点:规划阶段的目的不是找到某一个具体的运行方案,而是找到一个“在不确定性环境里韧性足够强”的投资方案。投资一旦落地,未来二十年的运行自由度就被限定死了。多阶段规划的核心逻辑也就清晰了——它把投资决策分成几个阶段,每个阶段之间允许根据已经实现的不确定性来调整下一步投资,相当于给决策者留了“后悔药”。当然,后悔药有代价,每多一个阶段,模型复杂度和求解难度都会成倍增加。
1.1 多阶段规划里的三类决策变量
在多阶段风光储规划里,决策变量大致可以分成三类。第一类是投资决策变量,比如某阶段是否新建一座风电场、扩建多少储能容量,这类变量通常是0-1整数或带整数步长的离散变量,因为它们代表物理上“建或不建”“扩多少套”的实体动作。第二类是运行决策变量,比如每个小时的储能充放电功率、卖给电网的功率、弃电功率,这类变量是连续的,在投资确定的条件下可以通过调度优化来求解。第三类是耦合变量,比如储能的老化状态、年度能量吞吐量、荷电状态跨时段衔接,它们连接了投资和运行两个层面的约束。
这三类变量的交互,是模型复杂度的主要来源。储能荷电状态会在不同阶段之间传递能量余量,某些投资约束(比如扩建储能必须在已有基础上进行)会让前后阶段的投资变量产生逻辑关联。因此,如果简单地按“阶段拆开、分步求解”,得到的结果大概率不是全局最优——因为早期阶段的运行决策会改变资源状态,进而影响后续阶段的可行域。
注意:我在做模型设计时,通常会把“跨阶段状态传递”单独抽象出来,用一组线性约束显式建模。如果不这么做,只是把各阶段目标函数简单加权求和,结果容易出现阶段间的重复投资或储能容量闲置,优化器给出的方案看起来漂亮,实际运行很尴尬。
1.2 确定性模型为什么不够用
有一种朴素的做法是,历史数据统计出典型日曲线,然后以典型日为输入跑确定性优化——风大光强就多充,风小光弱就少充,以此确定容量配置。这类方法在早期项目里很常见,因为建模简单、求解快,报告写起来也直观。
但在实际工程中,确定性模型最大的问题在于它把“预测偏差”当成了零。运行下来你会发现,实际风光出力与典型日曲线之间的偏离是常态而不是例外。当偏离导致某些时段储能充满,而后续还有大发电时段无法消纳,弃电就发生了;或者反过来,出力低于预期,需要从电网购电,购电价格高,经济性直线下滑。确定性模型给出的容量配置往往偏乐观,缺的正是那种“兜底能力”。
鲁棒优化恰恰是冲着这个问题来的。它在建模时就承认:我不知道未来确切的风光出力,但我可以通过一个不确定集合来限定它们的取值范围,然后寻找“即便不确定性落在集合内最坏位置、方案依然可行且最优”的配置。这种思路用在投资决策上非常自然——建设风电光伏是几十年的事,我不能赌未来每一年的风都按典型日吹。
2. 鲁棒优化在风光储项目里的建模细节
鲁棒优化的底层思想用一个生活类比来解释特别清楚:你规划一次自驾游,路线要走过一段山区,天气预报说有雨但你无法确定雨有多大。稳健的做法不是按晴天最优路线走,而是选择一条“即使下暴雨也能安全到达”的路线。代价可能是绕了路、多花了油费,但换来的是无论天气如何,你的旅行不会被取消。风光储规划里的鲁棒优化就是这个意思——最优性让位给可行性,追求的是在不确定扰动下依然能正常运转。
把这个思想转化成数学模型,需要解决三个问题:不确定性用什么集合来刻画、鲁棒优化模型长什么样、模型规模爆炸之后怎么求解。
2.1 不确定集合的三种选择
不确定集合的本质是“把哪些情形纳入最坏情况考虑”。集合太大会过度保守,因为你把那些概率极低、几乎不可能同时发生的情形也当成必须抵御的对象;集合太小则等于没做鲁棒化,方案关键时刻顶不住。实际项目里,我主要用三类集合。
盒式集合是最直接的一种,假设每个时段的风电出力落在预测值加减偏差的区间内:
[ \mathcal{U} = \left{ \xi_t \in [\hat{\xi}_t - \hat{\xi}_t \varepsilon, \hat{\xi}_t + \hat{\xi}_t \varepsilon], \ \forall t \right} ]
其中hat(xi)_t是预测出力,epsilon是偏差比例。这个集合很好理解,也方便写入约束,但很容易把所有时段都推到最坏情况——现实中不可能连续72小时都贴着下边界波动。直接用盒式集合的模型,算出来的储能容量往往大得离谱,项目可研都过不了。
椭圆式集合引入相关性,认为不确定参数之间存在协方差结构,集合形状从立方体变成了高维椭球。它能刻画“各时段出力偏差的相关性”,模型更精确,但对数据量的要求高,而且约束里出现二次项,混合整数问题求解难度上一个台阶。我在实际项目中通常只在数据充足、且需要精细刻画相邻时段相关性时用。
预算鲁棒集合是最实用的一种,在盒式基础上加一个总量限制,让最坏情况不能在所有时段同时发生:
[ \mathcal{U} = \left{ \xi_t \in [\hat{\xi}_t - \hat{\xi}_t \varepsilon, \hat{\xi}_t + \hat{\xi}_t \varepsilon], \ \sum_t |\xi_t - \hat{\xi}_t| / (\hat{\xi}_t \varepsilon) \leq \Gamma \right} ]
这个Gamma就是“预算”——它限制不确定参数偏离预测值的总时长或总量。Gamma=0退化为确定性模型,Gamma=全部时段数退化为最保守的盒式集合,实际中取时段数与场景特征之间的一个折中值。这个参数最大的好处是直观,你告诉工程师“我们允许连续8个小时同时出现最恶劣偏差”,他马上就能理解鲁棒模型的保守程度。
实操心得:我在光伏占比高的园区项目里通常把Gamma设为6~12小时(按日尺度),风电占比高时取12~24小时。原因是光伏出力偏差集中在白天,超过6小时的持续阴雨已经是极低概率;风电偏差则可能连续两三天维持高位或低位。这个值需要结合历史出力偏差的累积分布来标定,不能纯拍脑袋。
2.2 从两阶段到多阶段的模型演化
最简单的鲁棒优化结构是两阶段。第一阶段做投资决策,第二阶段在第一阶段方案已定的前提下,针对不确定参数的最坏实现做运行决策。写成一般形式就是:
[ \min_x c^T x + \max_{\xi \in \mathcal{U}} \min_y d^T y ]
subject toAx <= b,Bx + Cy <= h(xi_event)。中文读法就是:先决定建多大(x),然后大自然挑一个最不利于我的风和光(xi),我再想办法经济地调度(y)。
这个两阶段结构,放在风光储规划里恰好对应“投资-运行”两层关系。但实际项目的规划周期往往是滚动的,第一期建风电,运行两年看效果,再决定第二期是否扩建储能。这就是多阶段。多阶段模型写作一串嵌套的min-max-min结构:
[ \min_{x_1} \left( c_1^T x_1 + \max_{\xi_1} \min_{x_2} \left( c_2^T x_2 + \ldots + \max_{\xi_T} \min_{y} d^T y \right) \right) ]
也就是每一阶段的不确定参数实现之后,再决定下一阶段的投资——这比静止的两阶段模型更贴近工程实际,因为决策者确实会“边看边建,边建边调”。
多阶段模型的求解难度比两阶段高很多,因为每一层max-min都是一个独立的最优子问题,嵌套起来之后,整体问题的KKT条件都很难写。工程上我一般不会执着于严格求解完整的多阶段模型,而是把它转化成“滚动时域的序列两阶段问题”来近似——每一阶段只做未来有限时段的鲁棒决策,决策落地后滚动到下一个阶段。
2.3 列与约束生成算法的工程化应用
两阶段鲁棒优化目前比较标准的方法是列与约束生成(C&CG)。核心思想是让主问题先做一个初步的投资决策,然后把决策代入子问题,让不确定参数在集合里找最恶劣情形;找到之后把最恶劣情形对应的约束和变量加回到主问题,重新求解;循环往复,直到主问题和子问题的目标函数值收敛到同一个容忍度。这个过程实际上是在主问题和子问题之间来回迭代。
C&CG在实际工程里比Benders分解好用得多。Benders在每次迭代时只向主问题加一条割平面,收敛速度慢,而且割平面质量不佳时数值不稳定;C&CG每次迭代直接加一整组约束和变量,主问题的下界提升非常快。我做过一个96时段的园区微电网规划,两阶段模型配上C&CG,十几轮迭代就能收敛到1%的相对间隙,Benders跑同样的模型,迭代次数要多出好几倍。
多阶段模型应用C&CG的思路也是相似的——只不过内部嵌套了多个子问题,迭代逻辑变成“主问题给出一期决策,一期子问题找一期最恶劣场景,二期子问题在这个场景下给出二期最优决策,再把二期带回来的约束反馈给一期主问题”。这个过程说起来简单,但我调试时花了两周才把主问题的割集收敛条件调稳定。主要原因是不确定参数在不同阶段之间存在传递,子问题对偶变量的符号非常容易搞错。
3. 实操过程与核心环节实现
理论模型落到代码和求解器里,才是问题真正开始的地方。我从构建思路、参数设置到求解流程,把一套完整的实操过程拆给大家看。
3.1 电网结构假设与系统建模
先明确这个系统长什么样。我参考的项目背景是某工业园区,计划建一座风电场、一座光伏电站,配一套磷酸铁锂储能。园区负荷曲线有峰谷特征,风电和光伏出力有季节性差异。系统与外部电网的单点连接容量有限,超过这个容量就需要弃电或削减负荷。
这个系统用混合整数线性规划建模,决策包括:各期风电、光伏、储能的建设容量;各时段的储能充放电功率;弃电功率、从电网购电功率。不确定变量是风电和光伏的出力,历史预测误差用来构造不确定集合。
目标函数设置为总成本最小化,包括投资成本(设备单价×容量)、运维成本、购电成本、弃电惩罚成本,最后一项用来保证模型不会一味通过弃电来回避调节压力。
约束条件包括:
- 功率平衡约束:风电出力+光伏出力+储能放电功率+购电功率 = 负荷功率+储能充电功率+弃电功率
- 储能动态约束:荷电状态由上一时段荷电状态、充电/放电效率、自放电率共同决定
- 储能运行限值约束:充放电功率不超额定值,荷电状态保持在上下限之间
- 并网容量约束:购电功率不超联络线限值
- 投资扩张逻辑约束:后一期容量不低于前一期,扩建也只能在已有基础上进行
储能动态约束是这个模型里最容易被写错的地方,很多人直接写成SOC(t+1) = SOC(t) + eta*Pch(t) - Pdis(t)/eta,这在纯数学上是没错的,但实际求解时会出问题——充放电功率在同一个时段可能同时非零,造成不必要的能量损耗。处理办法是引入互斥约束加二元变量,或者更简单的,把充电和放电的效率直接做成同一方向的线性近似。我在实践中倾向于用大M法加二元变量来保证充放电不会同时发生,牺牲了一点速度但模型行为更符合物理逻辑。
3.2 时段聚类与典型日生成技巧
风光储多阶段规划不可能直接用8760小时为单位做优化,求解器根本无法承受。常规做法是先做时段聚类,用少量的典型日来代表全年运行特性。
我用的方法是k-means聚类或者更精细的k-medoids,特征维度包括风速、光照、负荷三条曲线。聚类做完之后,每个典型日带一个权重,全年运行约束就转成了典型日约束的加权组合。
但这里有一个容易被忽视的细节:用于鲁棒优化的典型日,不能像随机规划那样只用期望曲线。鲁棒模型需要的不确定集合包含“偏差范围”,因此聚类时除了保留典型日的均值序列,还必须保留每个典型日同类场景内各时段的偏差统计量。我在项目里是聚类后对每个典型日分别计算95%置信区间作为盒式集合边界,再叠加预算约束。
另一个实用技巧是聚类数目不能太贪多。我试过用16个典型日对全年数据进行聚类,求解时间比8个典型日涨了快十倍,但方案最优性改善只有2%左右。实际项目里8~12个典型日是性价比极高的区间,既能覆盖季节差异,又不会让模型变成求解器噩梦。
3.3 代码实现的关键帧:主问题与子问题循环
给一段简化的两张代码示意,帮助理解C&CG循环的实现骨架。这里用Python风格的伪代码,不依赖具体环境。
# 主问题初始化(只含第一阶段投资决策与基础约束) mp = MasterProblem() mp.add_vars(capacity_fixed=initial_guess) mp.solve() for iteration in range(max_iterations): # 将当前投资方案传入子问题 x_current = mp.get_solution() # 子问题: 在不确定集合中寻找最恶劣场景 sp = SubProblem(x=x_current) sp.solve() worst_xi = sp.get_worst_case_realization() sub_objective = sp.get_objective() # 上界与下界更新 ub = min(ub, mp.objective + sub_objective) lb = mp.objective # 收敛判断: 相对间隙足够小 if (ub - lb) / abs(lb) < tolerance: break # 把最恶劣场景对应的运行约束和变量加入主问题 mp.add_scenario_constraints(worst_xi) mp.solve()这段循环之所以能收敛,依赖一个事实:每轮迭代把最恶劣场景「固化」到主问题里,主问题的可行域随场景增加而收缩,目标值单调上升;同时子问题在最恶劣场景下给出的运行成本产生上界,上界与下界之间的间隙不断缩小,最终逼近全局最优。
现实实现中,真正难的不是循环本身,而是子问题的对偶推导。子问题内部如果存在离散变量(比如机组启停),就无法直接取对偶,C&CG在这里会失效。我在储能规划里为避免这个情况,会提前把储能运行约束线性化,避免在子问题里出现整数变量。这个决策在建模阶段就要想清楚,否则后面怎么调试都会在迭代间隙上卡住。
3.4 求解参数与收敛性把握
求解器参数设置对鲁棒优化模型的收敛速度影响很大。几个我觉得值得记下来的经验:
MIP Gap一般设置在0.5%~1%,太紧的间隙会让C&CG迭代次数显著增加。设置了5e-4的间隙在一个算例里跑了四十多轮不收敛,放到1%后七轮收敛,方案成本差异不到0.3%。工程决策完全不需要追求数学意义上的严格最优。
时间限制需要设置,我习惯在每轮主问题和子问题求解上各设30分钟上限。这个上限不是偷懒,而是保证整体求解进度可控,不会出现单场景卡死耗掉一整天的尴尬。
此外,子问题里不确定集合的极点搜索过程,初始化时可以用一个保守场景作为启动场景,而不要从确定性场景开始。这样第一轮迭代就能产生有意义的割集,减少无效迭代。
4. 常见问题与排查技巧实录
前面模型和算法讲了不少,但实际操作中真正折磨人的是各种调试问题。我把多次实践中遇到的典型问题整理成一份速查表,每一项都是我或合作过的团队真实碰到过的。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型求解结果明显偏保守,储能容量远大于预期 | 不确定集合Gamma值偏大,或盒式边界偏差取过头 | 减小预算参数,用历史数据的P90/P95分位而非最大值定义偏差 |
| 主问题目标函数不收敛,迭代间隙震荡 | 子问题对偶推导错误,或者子问题含整数变量无法取对偶 | 手动构造一个简单场景,逐项代入验证对偶乘子的符号 |
| 求解速度极慢,一轮迭代超过数小时 | 典型日数目过多、不确定场景耦合大量时段约束 | 压缩典型日数量,把96时段降为24时段做预测试,确认趋势后再还原 |
| 求解器报数值不稳定警告 | 大M值设置过大,或储能SOC递推方程出现极小数误差 | 根据设备容量重新估算M值,尽量取恰好大于物理上限的最小值;SOC约束改用毫安时或百分比归一化 |
| 方案里出现大量同时充放电 | 充放电互斥约束缺失,或者效率建模方式让同时充放电更“划算” | 检查二元变量和大M约束是否生效;用一组小规模数据单步调试充放电状态 |
| 某些典型日对应的不确定场景没有出现在最终割集中 | 不确定性集合极点搜索的枚举方式有遗漏 | 检查极点生成逻辑是否覆盖了盒式集合的所有角点、预算约束的交点;必要时用随机采样做交叉验证 |
这里挑两个最值得展开的说。
第一个是子问题对偶符号问题。C&CG循环里,主问题新增约束本质上是把子问题里最恶劣场景下对应的运行约束给包络化。如果对偶乘子符号搞反了,主问题新增的约束方向就会完全错误,轻则收敛到错误的解,重则主问题直接不可行。排查办法我建议用小学数学级别的办法:构造一个单时段、单储能的极端简化模型,手算一遍子问题最优解和对应的对偶乘子,然后在代码里输出子问题的对偶解逐项对比。这个笨办法至少帮我解决了四次莫名其妙的发散问题。
第二个是储能SOC漂移问题。多阶段滚动求解时,每一阶段结束时的SOC会被带到下一阶段作为初始值。如果历史阶段SOC数值处理不当(比如把百分比当成小数用),误差会逐阶段累积,最后可能让储能实际可用容量只剩名义容量的一半。排查办法是在每一轮迭代末尾强制输出SOC曲线,和物理直觉对照——如果SOC长期贴着上下限爬行,八成是递推公式或初值处理有问题。
5. 工具选型与环境搭建建议
鲁棒优化建模和求解,工具链选对能省一半时间。我按实际使用频率梳理一套组合。
5.1 建模层:选择顺手且容易调试的建模语言
目前我主要用两类。一类是通用代数建模系统,像YALMIP这类在矩阵层面处理问题很方便,尤其是在做对偶推导和不确定集合建模的时候,它帮你管理约束和变量索引,可以显著减少低级错误的发生。另一类是直接用Python的优化建模库来写,也需要配合求解器使用。两种方式我都常用的。
如果模型规模大、迭代环节复杂、需要和外部数据管道对接,Python生态通常更顺滑;如果是纯学术验证、需要快速做对偶变换验证,代数建模工具的方式在封装层面更友好。
提醒:不管用哪种工具,一定要在建模层把阶段编号、时段编号、不确定性事件编号的索引统一。我用过一个项目里三套编号系统并存的情况,浪费在索引对齐上的时间比求解本身还多。
5.2 求解器选型
求解器是模型能不能跑起来的关键。几个主流的商用和开源求解器我都用过,各有各的脾气。商用解算器在混合整数规划上表现非常稳,特别是对付大规模Benders/C&CG分解问题,数值稳定性和内存管理都让人省心。开源求解器在中小规模问题上也能胜任,但一旦出现不好处理的数值病态问题,调试成本会比商用求解器高不少,尤其是涉及大量大M约束时。
我的建议很简单:预算允许,优先商用求解器,配合多线程模式跑;预算有限,开源求解器也能用,但要把模型规模控制在它舒适的区域(变量数千级别、约束数万级别)。超过这个量级,你花在等结果上的时间成本可能远超软件授权费。
5.3 数据管线的坑
鲁棒优化模型不是只靠一组预测曲线就能算的,它需要历史运行数据、预测误差分布、负荷特性曲线等多路数据。我在实际项目中通常把数据预处理单独做成一个模块,输出统一格式的典型日场景集、偏差统计表、不确定集合参数。这样模型代码本身不接触原始数据,调试时可以快速替换数据来源而不污染模型逻辑。
一个特别值得注意的坑是时区对齐。光伏出力是以当地太阳时为基准的,而负荷数据往往用北京时间记录。新疆和内蒙的项目时差能到两小时,如果不对齐,光伏出力的峰值时段和负荷高峰完全错位,算出来的储能配置会和实际需求南辕北辙。这是我第一次做西北项目时踩过的坑,印象太深刻了。
6. 从模型到决策:这些事比求解本身更重要
模型求解出结果,不代表工作结束了。工程上还有两件看似简单、实则非常重要的事:鲁棒方案的保守度评估和结果可视化。
保守度评估的方法是对比同一项目分别用确定性模型、随机规划模型、不同Gamma值的鲁棒模型求解,把容量配置、总成本、最坏情况运行成本放在一张表里对比。这个对比的价值在于给决策者一个“成本-韧性”的权衡视角。我见过不少项目组直接拿着最保守的鲁棒结果给领导汇报,结果被质疑“为什么储能配这么多”,问题就出在缺少了这个评估环节。有了对比,项目组可以说“确定性方案省了5%的投资,但如果遇到十年一遇的连续阴天,运行损失会超过投资节省的两倍”,决策者自然会理解。
可视化上,除了常规的容量柱状图、SOC曲线,我强烈建议画一张「不确定性实现下的系统响应图」,把最恶劣场景的风光出力、负荷功率、储能充放电功率、弃电量画在同一张图表里。这张图是我向非技术背景的决策者解释鲁棒优化价值的利器——看着储能在最恶劣场景下依然能把功率缺口顶住,比任何数学公式都有说服力。
如果是做学术研究,还可以继续向随机鲁棒优化、分布式鲁棒优化、考虑寿命衰减的长期储能规划这些方向去扩展。把多阶段鲁棒优化和储能电池衰减模型耦合起来时,问题的复杂度会再上一个台阶,但理论上也更有意思。这些扩展方向说到底,都在回答同一个问题:面对不可完全预知的未来,怎样的前期决策能让系统活得久、跑得稳、亏得少。
最后分享一个我个人的小习惯。每次建完模型、跑通代码,我会刻意把不确定集合里的Gamma参数从小到大扫描一遍,把对应的最优容量配置轨迹画出来。这个轨迹能告诉我哪些容量配置在不确定性变化时是“稳健”的——如果某一种配置在Gamma从4变到12时基本不怎么变,那它大概率是比单纯最优解更值得选择的工程方案。这个习惯帮我避开过好几次“最优却脆弱”的陷阱,写在这里,就当是给同样在做风光储规划的朋友一个参考。