ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微电网两阶段鲁棒优化调度:关键场景辨别与CCG的Matlab实现

微电网两阶段鲁棒优化调度:关键场景辨别与CCG的Matlab实现 微电网调度圈里待久了你会发现一个特别现实的问题白天光伏出力猛涨傍晚负荷陡增加上电价波动这些不确定性叠加在一起让明天怎么排计划变成了一道特别头疼的题。很多刚接触这个方向的同学第一反应都是把预测曲线当成真实值直接做一个确定性优化模型。但实际跑下来就会发现预测误差稍微大一点日前计划到了日内执行阶段就完全走样不是需要高价购电就是被迫弃光切负荷。所以这两年两阶段鲁棒优化在微电网调度里越来越热。它的核心思想说白了就一句话把决策拆成日前定方案和日内做调整两段同时用不确定集合去覆盖所有可能出现的风光和负荷场景保证无论真实情况怎么变系统都能稳得住、经济性也可控。而关键场景辨别算法则是让这套鲁棒模型真正能落地求解的那把钥匙——它不需要枚举海量场景而是在迭代中主动找出最能挑战调度方案的那些极端但合理的场景用很少的计算量逼近最坏情况。这篇文章想跟你完整拆一遍这个项目两阶段鲁棒模型怎么建模、不确定集合怎么设、关键场景辨别算法和CCG求解框架是什么关系以及最实际的——Matlab里到底怎么写主问题和子问题、会遇到哪些坑。适合正在做微电网调度、综合能源系统优化或者被鲁棒优化搞得一头雾水的研究生和工程师参考。1. 微电网调度里不确定性到底难在哪1.1 确定性调度的事后打脸困境先从一个最直观的场景说起。假设你是一个微电网的调度员手头有光伏预报曲线、负荷预报曲线、分时电价目标是明天24小时的调度成本最小。最常见的做法是建一个混合整数线性规划模型把预测值当作真实输入求解得到每台机组出力和储能充放电计划。这个模型本身没什么问题线性规划求解也快。但问题在于预报永远是预报。光伏出力受云层遮挡影响五分钟内可能从满发跌到三成负荷曲线在高温天气下可能整体上移更别提电价在某些时段突然冲高。等第二天真正执行日前计划时你会发现实际运行点要么偏离计划太多导致网络潮流越限要么得频繁调用AGC机组去补缺口成本蹭蹭往上涨。这就是确定性调度的根本痛点**它假设世界会按预测剧本发展但真实世界不是这样演的。**你想要的是一个不管明天来什么天气、来什么负荷我都能扛得住的调度方案。1.2 从随机优化到鲁棒优化概率分布 vs 不确定集合处理不确定性有两条主流技术路线。随机优化考虑的是概率分布假设光伏出力、负荷都服从某种随机分布然后通过蒙特卡洛采样生成大量场景对每个场景分别求解再取期望成本。这个方法理论很优美但工程上有个硬伤——你很难准确获得新能源出力真实概率分布。拿光伏来说它的出力受季节、天气类型、云量、温度共同影响分布形态可能根本不是常见的正态分布。分布设错了优化结果自然不靠谱而且采样场景成千上万计算量也吃不消。鲁棒优化走的是另一条路不需要精确的分布信息只需要知道不确定参数大概会落在哪个区间范围内。这个范围就是不确定集合。优化目标变成在最坏情况下最小化总成本。换句话说我不关心每种场景发生的概率是多少我只要保证哪怕出现了集合里最恶劣的那种组合系统依然能安全运行且成本可接受。打个生活化的比方随机优化像是在准备一次出游根据天气预报判断大概率不下雨所以只带一把小伞鲁棒优化则不管预报怎么说都按可能会下暴雨来准备带一件防水冲锋衣。代价是方案会稍微保守一点但换来的是十足的底气。1.3 为什么两阶段结构天然适合微电网微电网调度天然分成两个决策阶段这是物理特性决定的不是人为硬凑的。第一阶段是日前决策要在不知道真实风光出力之前就拍板的事。最典型的是机组启停状态、与配电网的购售电计划、储能的充放电状态也就是要不要充、要不要放这类的0-1变量。这些变量一旦定了第二天很难临时改改了代价极高。第二阶段是日内调整在日前决策的基础上等真实出力出来后做一些连续量上的修正。比如机组出力微调、储能充放电功率调整、弃光弃风量、切负荷量等等。这些变量调整成本相对低频率可以很快。两阶段鲁棒优化做的就是在第一阶段决策时已经考虑到第二阶段会有最坏情况来捣乱所以第一阶段选择会留有一定的安全裕度。在数学上它就对应一个非常典型的min-max-min三层结构这个结构等下一节详细拆。2. 两阶段鲁棒调度模型的数学结构与物理含义2.1 模型框架min-max-min到底表达了什么两阶段鲁棒优化问题的标准形式是[ \min_{x \in X} \left{ c^T x \max_{u \in U} \min_{y \in F(x,u)} d^T y \right} ]这个式子乍一看有点劝退但逐层剥开其实很清晰。最外层的min是第一阶段决策x是机组启停、购售电状态等变量c^T x对应的是启动成本、固定运行成本这类定了就花掉的钱。中间的max是第二阶段的不确定性最大化给定第一阶段的x之后不确定参数u会在集合U里找一个让系统总成本最高的取值——这就是最坏场景。最内层的min是第二阶段的适应性调整在最坏场景u确定之后y是系统运行调整变量d^T y对应实时调整成本、弃风弃光惩罚、切负荷惩罚等。把这三层连起来读就通透了**决策者在离线阶段先做决定并假设在线阶段会遭到最坏自然情况的攻击于是在这个最坏情况下做最优调整所有可能的坏情况里取成本最大的那个作为最终优化目标。**求解出来的x就是能在所有不确定情况下都站得住的方案。2.2 第一阶段和第二阶段变量及约束对应以典型的微电网系统为例设备通常包括光伏PV、风电WT、储能ESS、燃气轮机MT、与主网的联络线加上本地负荷。第一阶段变量日前决策燃气轮机的启停状态 (z_{mt,t} \in {0,1})储能充电状态 (z_{ch,t} \in {0,1})、放电状态 (z_{dis,t} \in {0,1})与主网购电状态 (z_{buy,t} \in {0,1})是否切负荷 (z_{cur,t})如果有切负荷决策的话这些0-1变量决定了系统的基本运行方式对应的约束包括机组爬坡约束储能SOC动态方程及充放电状态互斥约束购售电状态互斥约束机组最小启停时间约束视模型复杂度可选第二阶段变量日内调整燃气轮机实际出力 (P_{mt,t})储能充放电功率 (P_{ch,t}, P_{dis,t})购售电功率 (P_{buy,t}, P_{sell,t})弃风弃光量 (P_{cur}^{pv,t}, P_{cur}^{wt,t})切负荷量 (P_{cur}^{load,t})各节点相角或潮流如果做网络约束这些连续变量对应的约束包括功率平衡、设备容量上下限、储能SOC连续演化等。这里要特别注意第一阶段的0-1变量是硬承诺第二阶段的连续变量是软调整。储能状态变量的处理尤其关键——如果你把存储充电状态也做成0-1那第一阶段就要决定明天第5个小时充不充电这个决定在第二天是不能反悔的属于风险管理意义上的commitment如果你把存储充放电功率当作连续变量在第二阶段优化它就变成了实时调节手段。两种建模方式带来的保守性差异很大。我自己的项目经验是微电网规模不大时把充放电状态放进第一阶段更贴近实际调度员决策逻辑模型也更保守、更安全。2.3 不确定集合构建与预算参数设置的艺术不确定集合是鲁棒优化的灵魂。集合太大会让结果特别保守成本高得没法接受集合太小又起不到保护作用。最常用的集合形式是盒式集合加预算约束[ U \left{ u \in \mathbb{R}^{K \times T} \mid u_{k,t}^{min} \leq u_{k,t} \leq u_{k,t}^{max}, \quad \sum_{t1}^{T} \frac{|u_{k,t} - \hat{u}{k,t}|}{\Delta u{k,t}} \leq \Gamma_k \right} ]其中 (\hat{u}{k,t}) 是第k个不确定源的预测值(\Delta u{k,t}) 是最大偏差量(\Gamma_k) 是预算参数。预算参数Γ决定了最坏场景能坏到什么程度。Γ0时就是完全确定性问题Γ等于T时等价于每个时段都取极端值结果会非常保守。实际工程中Γ一般取2到4意思是整个调度周期中最多有2到4个时段同时取到极端偏差这一般就能覆盖大部分恶劣天气或负荷突变的场景了。在Matlab里构建这个集合时需要注意鲁棒优化的不确定集合不需要显式枚举所有场景它只是给后续的求解算法提供一个范围和一个总偏差约束。真正的难点在求解时怎么让最坏场景被提炼出来这就是关键场景辨别算法的用武之地。3. 关键场景辨别算法与CCG框架到底解决了什么3.1 为什么不能靠暴力枚举场景很多人第一次接触两阶段鲁棒时脑子里冒出来的第一个想法是既然不知道哪个场景最坏那就把所有可能的场景都列出来然后逐个优化不就行了。但算一下复杂度就懂了。假设光伏有24个时段每个时段可能取预测值、上偏差、下偏差三档那场景数量是 (3^{24})约2820亿个场景。哪怕每个场景只需要1毫秒求解也需要9000年。这还没算风电和负荷。所以枚举走不通必须要有一套机制在求解过程中主动找出值得关注的场景。3.2 从列与约束生成CCG理解算法骨架列与约束生成Column and Constraint GenerationCCG是解决两阶段鲁棒问题的主流算法框架。核心思路可以类比成一个循环博弈先用一个初始场景建一个简化版主问题求出初步调度方案然后拿这个方案去测试看看在不确定集合里能不能找到某个场景让当前方案失效如果找到了就把这个新场景的约束塞进主问题里重新求解。反复迭代直到再也找不到能让当前方案变差的场景为止。CCG流程抽象起来是四步写成伪代码是这样初始化给定初始场景集合 S{s0}设置 LB-∞, UB∞, 迭代序号 k1 循环 1. 求解主问题MP得到最优解 x_k 和目标值 obj_MP LB obj_MP 2. 固定 x_k求解子问题SP得到最坏场景 u_k 和第二阶段成本 Q(x_k) UB min(UB, c^T x_k Q(x_k)) 3. 若 UB - LB ε终止循环返回 x_k 4. 否则将新场景 u_k 对应的变量和约束加入主问题kk1返回步骤1主问题在第k次迭代的形式是[ \min_{x, y_1, ..., y_k} \left{ c^T x \sum_{i1}^{k} d^T y_i \right} ]约束包括第一阶段约束以及针对每个已发现场景 (u_i) 的第二阶段可行性约束[ G y_i \geq h - A x - B u_i, \quad \forall i 1,2,...,k ]这里的关键是**每发现一个关键场景就往主问题里增加一组对应于这个场景的第二阶段变量和约束。**所以主问题规模会随迭代次数缓慢增长但增长的场景数量通常只有几位数远小于暴力枚举。3.3 关键场景辨别算法在框架里的位置市面上很多论文声称用了关键场景辨别算法或者场景压缩其实本质上都是在CCG的子问题解算环节做文章。子问题的内部结构是max-min两层嵌套求解思路通常是先写出内层min的对偶形式把它转成一个max问题然后和外层max合并成单层max问题。合并后的形式变成[ \max_{u, \lambda} \left{ \lambda^T (h - A x - B u) \right} ]其中λ是对偶变量同时受对偶可行域约束u则来自不确定集合U。这个优化问题的主变量是u和λ而λ和u的乘积产生了双线性项。处理双线性项有几个流派。最正统的做法是借助大M法引入二进制变量把双线性项线性化。对每个不确定参数 (u_t)引入一个二进制变量来表示它是否取偏差上界然后通过大M约束把 ( \lambda^T B u ) 线性化。因为不确定集合通常是有限的顶点组合最坏解必然出现在某个顶角上所以这种线性化在数学上是精确的不损失最优性。然后会发现合并后的单层问题是一个MILP问题可以直接丢给求解器去解。求解器返回的那个u就是当前调度方案x下最坏场景。而关键场景辨别的名字说的就是子问题解出来后挑出这个u的过程——它不一定出现在初始场景里而是在每次迭代中被辨别出来的新场景。3.4 CCG和Benders分解的对比很多人问为什么用CCG而不是传统的Benders分解。两者的区别在于反馈的信息不同。Benders分解子问题返回的是一个割平面即对当前主问题方案的一个线性约束修正它不显式返回场景。每轮迭代只更新一条不等式约束。优点是主问题始终是原问题规模的连续版本不膨胀缺点是收敛往往比较慢尤其在整数变量较多时需要的割平面数量很大。CCG返回的是一组具体的第二阶段变量及其对应约束等于把一个完整场景的细节全暴露给主问题。主问题规模会逐轮增大但换来的是少得多的迭代次数。在微电网这种0-1变量不太多一般几十到几百个的场景下CCG的收敛速度优势非常明显。维度Benders分解CCG子问题返回信息一条割平面对偶信息新场景及其完整约束集主问题规模基本不变逐轮增加迭代次数通常较多通常较少对整数变量的处理较弱天然适配两阶段结构工程实现难度中等中等子问题需对偶处理我的实测感受微电网规模不太大时CCG通常5到10轮就收敛了而Benders有时要跑几十轮。关键场景辨别算法本质上就是CCG框架下的一个场景筛选器让每一轮迭代都打在调度方案的软肋上。4. Matlab代码实现拆解从模型到可运行程序4.1 整体代码结构与运行流程这套Matlab实现我习惯按模块拆分分成四个文件方便调试main.m主入口定义系统参数、不确定集合、调用CCG循环build_MP.m构建主问题输入是已发现场景集合输出YALMIP模型solve_SP.m构建并求解子问题输入是固定的第一阶段决策输出最坏场景和第二阶段成本plot_results.m可视化结果绘制机组出力、储能SOC、购售电曲线等数据结构上我习惯把全部系统参数放进一个结构体para里包括负荷预测值、光伏预测值、风电预测值、偏差界限、储能参数、机组参数、电价等。这样主问题和子问题函数都能直接引用不容易传参出错。4.2 主问题MP的YALMIP建模要点主问题的核心代码框架大致是% 第一阶段变量 x_start binvar(n_unit, T); % 机组启动状态 x_stop binvar(n_unit, T); % 机组停机状态 z_mt binvar(n_unit, T); % 机组运行状态 z_ch binvar(1, T); % 储能充电状态 z_dis binvar(1, T); % 储能放电状态 z_buy binvar(1, T); % 购电状态 % 第二阶段变量针对每个已发现场景 k 定义一组连续变量 Y {}; % cell数组存放各场景的第二阶段变量 for k 1:n_scen_cur Y{k}.P_mt sdpvar(n_unit, T); Y{k}.P_ch sdpvar(1, T); Y{k}.P_dis sdpvar(1, T); Y{k}.P_buy sdpvar(1, T); Y{k}.P_sell sdpvar(1, T); Y{k}.Pcut_pv sdpvar(1, T); Y{k}.Pcut_load sdpvar(1, T); end % 目标函数 obj sum(sum(c_start .* x_start)) sum(sum(c_stop .* x_stop)) ... sum(sum(c_mt * ones(size(z_mt)) .* z_mt)); for k 1:n_scen_cur obj obj sum( c_MC .* Y{k}.P_mt ) ... sum( c_ESS .* (Y{k}.P_ch Y{k}.P_dis) ) ... sum( price_buy .* Y{k}.P_buy ) - ... sum( price_sell .* Y{k}.P_sell ) ... C_curtail * sum(Y{k}.Pcut_pv) ... C_cut_load * sum(Y{k}.Pcut_load); end Constraints []; % 第一阶段约束机组爬坡、启停互斥、储能状态互斥、购售电互斥 Constraints [Constraints, z_ch z_dis 1]; Constraints [Constraints, z_buy z_sell 1]; % 储能SOC初值约束 Constraints [Constraints, SOC(:,1) SOC_init ...]; % 第二阶段约束对每个场景分别约束功率平衡 for k 1:n_scen_cur u scen_set{k}; % 第k个场景的不确定参数取值 Constraints [Constraints, Y{k}.P_mt Y{k}.P_dis Y{k}.P_buy u.P_pv u.P_wt ... u.P_load Y{k}.P_ch Y{k}.P_sell Y{k}.Pcut_pv Y{k}.Pcut_load]; Constraints [Constraints, Y{k}.P_mt P_mt_min .* z_mt]; Constraints [Constraints, Y{k}.P_mt P_mt_max .* z_mt]; % ... 其他容量约束、SOC演化约束 end ops sdpsettings(solver, cplex, verbose, 0); optimize(Constraints, obj, ops);有几个容易踩的坑需要提醒第一第二阶段变量的下标k必须和场景数对应不能一个变量反复用。每次迭代新增场景时要用length(scen_set)1来扩展。第二机组启停状态和第二阶段出力之间的耦合是通过 (P_{mt,t} \leq P_{mt}^{max} \cdot z_{mt,t}) 这种大M式约束建立的。如果z_mt是0出力强制为0如果z_mt是1出力在容量范围内自由调整。这组约束每个场景都要写不能只写在主问题第一阶段。第三储能SOC的状态演化约束建议放在各场景各自独立实现。因为不同场景下充放电决策不同SOC轨迹自然不同。把SOC约束放在第一阶段反而会过度收紧。4.3 子问题SP的对偶变换与线性化处理子问题要解决的是固定x后找出让总成本最大的u和对应的第二阶段最优解。核心代码如下function [u_worst, Q, status] solve_SP(x_val, para) % 读取固定后的第一阶段决策 z_mt_val value(x_val.z_mt); z_ch_val value(x_val.z_ch); z_dis_val value(x_val.z_dis); % 第二阶段原始变量用于恢复y值 y sdpvar(2*n_var, 1); % 包含所有第二阶段变量 % 不确定变量 u sdpvar(n_unc, T); % 约束条件 Constraints []; % 不确定集合约束 Constraints [Constraints, u_min u u_max]; Constraints [Constraints, sum(abs(u - u_hat) ./ delta_u) Gamma]; % 第二阶段约束A*x B*u C*y d % 对偶变量 lambda 0 lambda sdpvar(n_constr, 1); Constraints [Constraints, lambda 0]; Constraints [Constraints, C * lambda d]; % 对偶可行域 % 目标最大化 lambda * (d - A*x_val - B*u) obj lambda * (d_val - A*x_val); obj obj - lambda * B * u; % 双线性项 % 大M法处理双线性项 lambda * B * u % 简化处理因为 u 的每个分量在盒子顶点取最值 % 引入二进制变量 v 表示取上界还是下界 v binvar(n_unc, T); Constraints [Constraints, u u_hat (2*v-1) .* delta_u]; Constraints [Constraints, sum(v) Gamma]; % 引入辅助变量 eta lambda * B * u线性化 % 实际中可以定义一个aux变量通过线性化工具箱处理 % 这里简化为直接使用临时变量 ops sdpsettings(solver, cplex, verbose, 0); optimize(Constraints, -obj, ops); % YALMIP默认最小化所以取负号 end这里我给的是结构示意实际工程中线性化双线性项 ( \lambda^T B u ) 需要更细致地展开对每个不确定参数 (u_{k,t})定义二进制变量 (v_{k,t} \in {0,1})其中1表示取上偏差0表示取下偏差。于是 (u_{k,t} \hat{u}{k,t} (2v{k,t}-1)\Delta u_{k,t})。双线性项 ( \lambda_i \cdot u_{k,t} ) 展开后变成 ( \lambda_i \cdot \Delta u_{k,t} \cdot (2v_{k,t}-1) \lambda_i \cdot \hat{u}{k,t} )。其中 ( \lambda_i \cdot v{k,t} ) 是逻辑与AND形式的乘积项。这个可以用M法线性化引入辅助变量 ( w_{i,k,t} \lambda_i \cdot v_{k,t} )约束[ 0 \leq w_{i,k,t} \leq \lambda_i^{max} v_{k,t} ] [ \lambda_i - \lambda_i^{max}(1 - v_{k,t}) \leq w_{i,k,t} \leq \lambda_i ]这里 (\lambda_i^{max}) 是对偶变量的上界可以用对偶可行域事先估算或者取一个足够大的数。4.4 初始场景选择与收敛判据的设置初始场景的选取对迭代收敛速度有直接影响。我的建议是**把预测场景所有不确定参数取预测值作为第一个场景加入主问题。**这样主问题第一轮优化出来的方案至少对平均情况是可行的后续迭代才在平均方案基础上逐步加严。第2个、第3个初始场景可以选择光伏下偏差负荷上偏差最缺电场景以及光伏上偏差负荷下偏差最富余场景这两个极端场景能提前约束主问题减少迭代次数。收敛判据我建议同时使用绝对间隙和相对间隙gap (UB - LB) / max(1, abs(UB)); if gap epsilon || (UB - LB) abs_tol break; end绝对间隙取1e-3到1e-4之间比较合适。太小了浪费时间太大会导致方案在实际最坏场景下不够安全。5. 实测中的坑与调参经验5.1 子问题的求解速度陷阱两阶段鲁棒优化的计算瓶颈大多数时候不在主问题而在子问题。因为子问题是内层带双线性项的MILP规模虽然不大但每一轮都要求解一次。迭代10轮就意味着至少10次MILP求解。加速手段有几个一是给对偶变量lambda设置合理的初始解。可以利用上一轮子问题的lambda值作为下一轮的热启动初值。CPLEX和Gurobi都支持warm start能省掉大量分支定界搜索。二是合理简化对偶可行域。有些约束在物理上根本不会成为紧约束可以视情况去掉减小子问题规模。但这个方法要小心去掉约束可能让求出的最坏场景变得太坏导致主问题方案过度保守。我的经验是先保留全部约束跑一遍看哪些约束的对偶变量始终为0再决定是否删。三是用强对偶替换KKT条件。在子问题都是线性规划的前提下KKT条件和强对偶定理给出的是同一个最优性描述。强对偶表述更简洁只需要把原始变量和对偶变量同时写进模型然后用原对偶间隙为0作为约束。很多刚上手的人写KKT条件时容易把互补松弛条件写错写成两个不等式的乘积导致模型变非线性。强对偶的好处是互补约束被替换成了目标值相等线性性质保持得很好。5.2 求解器选型与参数设置的实测对比两阶段鲁棒模型本质上是一个MILP套MILP的结构。求解器选型直接影响计算速度。求解器主问题MILP子问题MILP备注CPLEX表现稳定行业标杆与YALMIP配合最好Gurobi速度极快非常快大模型推荐Mosek连续线性规划出色MILP一般不推荐做MILP内置linprog不支持0-1变量-只适合纯LP我自己的实测是同样的微电网模型4台机组、24时段、10种设备Gurobi求解子问题比CPLEX快大约20%到30%但CPLEX在添加了大量场景约束后的主问题上表现更稳内点法迭代更少。如果精度要求不是特别高Gurobi通常更快到达收敛。YALMIP参数设置上有两个很值得调的选项ops sdpsettings(solver, gurobi, verbose, 0); ops.gurobi.MIPGap 1e-3; ops.gurobi.MIPFocus 2; % 让Gurobi优先改进下界加速收敛 ops.gurobi.TimeLimit 300; % 防呆设置MIPFocus2对两阶段鲁棒这种需要反复求解同构MILP的场景效果显著因为下界提升速度直接影响外层迭代次数。5.3 大M参数的选择不能乱设双线性项线性化时用到大M参数这个参数非常关键。M太大会引发数值灾难卡在求解器里半天出不来M太小可能把可行域割掉得到错误的最优解。对 (\lambda_i^{max}) 的估计方法从对偶可行域 (C^T \lambda \leq d) 出发把每个λ单独界定。实际工程中可以从第二阶段约束的物理意义出发进行估算。比如功率平衡约束的对偶变量物理含义是节点边际电价那它的上界就不会超过尖峰电价再加一个备用价格惩罚因子取个几百上千就足够了。我调试时习惯的做法是先跑一次确定性版本观察对偶变量lambda的量级然后给lambda上界设置成8到10倍于观察到的最大值。这样既能保证线性化不割可行域又能避免M过大的数值问题。5.4 避免储能SOC失真的一个关键细节储能SOC的建模在上面的主问题框架里容易出问题。很多初学者写SOC时这样写SOC(:, t1) SOC(:, t) (eta_ch * P_ch(:, t) - P_dis(:, t) / eta_dis) * dt;这句在确定性模型里没问题但在两阶段模型里有个隐性假设SOC是一个跨阶段状态变量它把不同场景串起来了。但不同场景下的充放电决策是不同的如果把SOC定义成全局共享变量那第1小时在场景A的SOC会影响到第2小时场景B的可用容量这在物理上是错的——因为第1小时真实发生时你只会经历一个场景不会同时经历A和B。正确的做法是SOC在各场景内独立定义每个场景有自己的SOC轨迹场景之间不共享SOC状态。这相当于假设日前调度计划给定后日内各场景的SOC轨迹是根据该场景实际充放电重新算出来的。在CCG框架里SOC变量应放在每个场景的第二阶段变量集合中而不是第一阶段。这个细节抓不好你会得到一个偏乐观的方案储能容量会被不同场景重复利用导致实际运行中SOC越限。5.5 关键场景辨别算法的改进方向标准CCG每轮只加入一个最坏场景。实际中还有一种更高效的变体子问题求解时不仅记录最坏场景还记录当前最优解下的若干个次优场景一并加入主问题。这样每轮迭代能一次性覆盖更多极端情况典型能减少30%到50%的外层迭代次数。具体实现在YALMIP求解完子问题后用getsol或recover拿到最优解后再做一次邻近场景扰动比如对最坏场景u*的每个分量分别取上界和下界的组合生成K个候选场景逐一检查是否会让当前主问题方案不可行或显著提高成本只加入那些会咬人的场景。这个改进方向在光伏、风电、负荷三类不确定源并存时效果特别好因为最坏组合往往不是唯一确定的提前把几个同样坏的场景压进主问题能让调度方案更均衡。6. 最后聊聊这套代码的实际运行体验我调试这套程序时有一个特别深刻的体会**两阶段鲁棒模型的调试顺序一定要从简到繁。**别一上来就上全部设备和全部约束否则报错都分不清是建模问题还是代码问题。正确路径是先跑纯确定性模型确认目标函数值合理、各种约束的松紧状态符合直觉然后在确定性模型基础上只加入盒式不确定集合但用暴力枚举少量场景来做校验确认CCG结果和枚举结果一致最后再逐步加入预算约束、更复杂的不确定源和更多设备每加一块就跑一遍对比。这样做的好处是每一步都能对照已知正确答案出问题能快速定位。另外一个感受是Γ的取值对方案保守性影响极大。同样是25%的光伏波动幅度Γ取2时总成本比确定性模型只高出2%到4%Γ拉到8时会高出15%以上。实际工程项目里这个参数的选取建议结合历史运行数据进行回溯测试跑过去一年的实际风光出力数据看设定Γ后系统有多少概率会出现超出鲁棒包络的工况。目标应该是把这个概率控制在1%以下。两阶段鲁棒模型还可以朝随机鲁棒方向扩展比如把场景概率附加到不确定集合上让最坏场景还受到概率权重约束既能控制极端保守性又不丢失鲁棒保护能力。这套Matlab代码框架其实已经预留了接口加入场景概率后子问题的目标函数会从max变成加权max实现难度并不大。如果你正在实验室里被这个方向折磨先别急把CCG框架理解透再用Matlab把主问题-子问题这对循环跑通剩下的事情就是不断往模型里加设备、加约束、加场景了。等成功跑出第一条收敛曲线的时候你会觉得这个min-max-min其实没那么可怕。
返回列表