做微电网调度的朋友应该都有过这种经历:按照光照预测把机组启停、储能充放电计划排得漂漂亮亮,结果第二天上午十点一片云飘过来,光伏出力从1000kW瞬间掉到300kW,整个日前计划全部作废,只能转入实时调整。这一调整,购电成本上去了,切负荷的惩罚成本也出来了,调度员还得问你为什么“偏差这么大”。
这篇内容要说的两阶段鲁棒优化经济调度方法,就是专门对付这种“预测赶不上变化”的问题。它不追求用预测值做一套完美的计划,而是先做日前决策(第一阶段),再假设不确定性在最坏方向发生,在第二阶段用最小成本调整来兜底。整个求解用C&CG(列与约束生成)算法完成,Matlab + Yalmip + CPLEX/Gurobi可以完整复现。如果你的方向是微电网优化调度、鲁棒优化入门,或者正在复现论文代码,这篇文章的模型推导、代码结构和调试经验应该能帮你省掉不少无效试错。
我在这个项目里做的是升级优化版,相比基础版改进了储能建模、分时电价下的交互策略和滚动时域执行逻辑,后面会逐一展开。
1. 微电网调度里的不确定性:为什么确定性模型不够用
1.1 微电网的基本结构和调度任务
先把对象说清楚。我这里的微电网是典型结构:光伏(PV)、微型燃气轮机(MT)、储能电池(BESS)、本地负荷,再通过一个公共连接点(PCC)与外部电网进行功率交换。调度周期一般是24小时,分辨率取1小时,少数精细化场景取15分钟。
调度任务就是回答三个问题:每个时刻MT发多少电?储能充还是放、功率多大?从电网买电还是卖电、交换功率多少?如果只考虑这些,那它就是一个标准的混合整数线性规划问题,Matlab里用Yalmip调CPLEX十几秒就能出结果。但实际问题恰恰没这么简单——光伏出力和负荷预测都不可能是完美的。
1.2 确定性模型为什么会在真实运行中“翻车”
确定性模型把光伏出力和负荷当成已知参数,目标函数里只有第一阶段的发电成本、购电成本和储能折旧成本。这样优化出来的方案看起来很美:分时电价谷段充电、峰段放电,燃气轮机配合爬坡,一切严丝合缝。
但到了实际执行那天,光伏预测误差动辄10%-20%,极端天气下可能半小时内骤降50%以上。如果日前计划里写了“正午光伏满发,燃气轮机停机,储能充电”,实际光伏只有预测的一半,那功率平衡瞬间破掉,唯一的临时手段就是高价网购电或切负荷。这个额外支出,在确定性模型里是完全看不见的。
所以传统确定性调度的症结在于:所有约束都建立在“预测准确”这个隐含假设上。一旦假设不成立,计划就失去了可执行性。
1.3 鲁棒优化 vs 随机优化:工程场景下怎么选
处理不确定性的主流思路有两条路。随机优化需要给出不确定量的概率分布,然后用场景抽样或者机会约束来建模,问题规模会成倍增长,而且分布参数不准时效果也会打折。鲁棒优化不需要概率分布,只需要知道不确定量的波动范围,它要找的是“在所有可能场景下都不会出大问题的方案”。
微电网的实际工程场景里,光伏出力的准确概率分布很难拿到,但上下限很容易从历史数据估计,所以鲁棒优化是很自然的选型。两阶段鲁棒优化又比单阶段鲁棒优化灵活:第一阶段做“现在必须拍板”的决策,第二阶段等不确定性实现后再做低成本调整,这样不会为了极端场景牺牲所有经济性。直观理解就是:先定策略,再留调整余地。
2. 两阶段鲁棒模型的数学架构:min-max-min到底在干什么
2.1 模型的整体框架
两阶段鲁棒优化的目标函数写出来是这个结构:
min x ( 第一阶段成本 + max u∈U min y 第二阶段成本 )翻译成人话就是:我先决定第一阶段变量 x(机组启停、日前购售电计划、储能基准充放电计划),然后大自然会挑一个最不利的光伏出力场景 u 来坑我,看到 x 和 u 之后我再决定第二阶段调整变量 y(机组实际出力、弃光量、切负荷量、储能修正功率),目标是让最坏场景下的总成本最小。
三层结构里,min 是最内层的运行优化,max 是中间的不确定性对抗,最外层 min 是策略优化。每一层都有自己的约束和变量,如果直接丢给通用优化器,当前主流的求解器并处理不了这种三层嵌套问题,所以才需要用C&CG算法把它拆开迭代求解。
2.2 第一阶段决策变量和约束
第一阶段变量包括:MT机组的启停机状态、MT的基准出力、通过PCC与外部电网的购售电基准功率、储能电池的基准充放电功率。这些变量对应的是“日前计划”,需要在不确定性实现之前定下来,不能事后反悔。
第一阶段约束主要有几类:
- MT出力上下限约束:每个时刻的基准出力在机组额定范围内。
- 启停状态约束:同一时刻不能既启又停;最小运行时间和最小停机时间约束如果考虑,会额外增加大量整数变量,初期代码可以先不加。
- 爬坡约束:MT相邻时段出力变化不能超过爬坡速率。
- PCC功率约束:购电功率和售电功率不能同时为正,且不能超过联络线容量。
- 储能基准功率约束:充放电功率有上限,且同一时刻不允许既充又放。
这些约束用Yalmip写起来比较直接,难点在之后要把它和第二阶段场景耦合。
2.3 第二阶段决策变量和约束
第二阶段变量是在第一阶段变量 x 和不确定场景 u 都确定之后才出现的,包括:MT实际出力、储能实际充放电功率、弃光功率、切负荷功率、以及和电网的实际交换功率调整量。
第二阶段约束中最核心的是功率平衡方程,每个时段必须满足:
光伏实际出力 + MT实际出力 + 储能放电功率 + 购电功率 = 本地负荷 - 切负荷量 + 储能充电功率 + 售电功率这个等式右侧或左侧会因为不确定场景里光伏出力变化而被打破,第二阶段就通过调整MT出力和储能功率、必要时弃光或切负荷来重新平衡。如果第二阶段无解,意味着第一阶段方案在某个场景下不可行,这就是C&CG里需要生成可行性割的原因。
此外储能还要满足SOC动态方程,也就是荷电状态随充放电功率和时间步长的演化关系。储能这部分恰恰是最容易埋坑的地方,SOC上下限、充放电效率非对称、初始和终点SOC约束,任何一个写得不对,结果都会不合理。
2.4 不确定集怎么构造:盒式加预算约束
两阶段鲁棒的不确定源主要是光伏出力,建模时最常用盒式不确定集,也就是:
U = { u : |u_t - u_t^forecast| <= Δ_t, t = 1, ..., T }其中 Δ_t 是每个时段的最大偏差量,通常取预测值的10%-20%。但盒式集合的问题是它允许所有时段同时偏离到极端值,这种场景在现实中几乎不会出现,导致结果过于保守。
工程上更常用的是加预算约束(budget of uncertainty):
sum_t ( |u_t - u_t^forecast| / Δ_t ) <= ΓΓ就是预算参数,它的取值范围是0到T。当Γ=0时,不确定集退化为预测单点,模型退化成确定性模型;当Γ=T时,等于允许所有时段都取极端值,保守度最高。实际调参时一般从T/3到2T/3之间去试,经济和鲁棒性相对平衡。
3. C&CG算法求解:把三层优化拆成主问题和子问题的迭代
3.1 为什么用C&CG而不是直接解原问题
三层嵌套的min-max-min结构没有任何通用求解器能直接高效求解。主流的算法有两种:Benders分解和C&CG(Column and Constraint Generation)。C&CG的核心思路是用一组“典型恶劣场景”逼近不确定集,主问题在有限个场景下求最优解,子问题在给定第一阶段解的情况下找当前最大威胁的场景,找到之后把这个场景加进主问题继续迭代。它的收敛速度通常比Benders分解更快,因为每轮迭代都往主问题里添加原问题的完整变量和约束,割平面更紧。
3.2 主问题(Master Problem)的构建
主问题是在一组已知的不好场景 u^(1), u^(2), ..., u^(k) 下求最小总成本的优化问题,其中每个场景对应一套第二阶段变量 y^(j)。
主问题写成:
min c^T x + η s.t. x ∈ X η >= d^T y^(j) y^(j) ∈ Y(x, u^(j)) j = 1, 2, ..., k其中η是辅助变量,表示对最坏场景第二阶段成本的估计。每轮迭代会把新发现的恶劣场景u^(k+1)追加进去,主问题规模会逐步变大。
第一轮迭代时的主问题可以不包含任何场景,直接把初始场景设为预测值场景u^(0)。之后的C&CG迭代会不断往主问题里添加新场景和对应的第二段变量约束。
3.3 子问题(Subproblem)怎么通过强对偶求解
主问题求解后得到第一段变量 x*,子问题要回答的是:在x*固定的情况下,哪个不确定场景会让第二阶段成本最高。子问题写成:
max u∈U min y∈Y(x*, u) d^T y内层的min是一个线性规划,满足强对偶条件时,可以把内层min替换成它的对偶max,这样整个子问题就变成了单层max问题。具体做法是:把第二段约束写成矩阵形式,引入对偶变量λ,内层对偶之后,原来的max-min问题变成:
max λ^T (g - B x* - D u) s.t. E^T λ <= d λ >= 0 u ∈ U这里难点在于目标函数里出现了λ和u的乘积项,也就是双线性项,这不再是一个线性规划。解决办法是用大M法引入辅助变量做线性化:对每一对λ_i × u_t,引入新变量v_it,同时加一组包含M的大约束来模拟乘积关系。大M的取值需要谨慎,我后面专门说这个坑。
求解完子问题后,得到的u就是当前最恶劣场景,子问题目标函数值Q(x)就是最坏情况下的第二阶段成本。这时上界更新为:
UB = min(UB, c^T x* + Q(x*))下界更新为主问题目标函数值LB。当(UB - LB) / UB小于阈值时,迭代终止。主问题原场景中收集U中每个极端点,目标函数下界η会不断逼近真实最优值。
3.4 C&CG迭代的完整流程
用步骤描述整个求解流程:
- 初始化:设置下界LB=-∞,上界UB=+∞,迭代计数k=0,选初始场景u^(0)为预测值场景。
- 求解主问题:得到第一阶段解x和辅助变量η。更新LB = max(LB, c^T x* + η*)。
- 固定x*,求解子问题:得到最恶劣场景u和子问题目标值Q(x)。
- 更新UB = min(UB, c^T x* + Q(x*))。
- 计算相对间隙gap = (UB - LB) / UB,如果gap小于设定的阈值(比如1%),终止迭代。
- 否则把u*作为新场景添加到主问题中,对应新增一组第二段变量y^(k+1)和相关约束,k = k + 1,回到步骤2。
这套流程看起来简单,实际落地时代码里最容易出错的地方在主问题场景递增时的变量命名和约束追加,不注意就会把Yalmip的变量搞混。
4. Matlab代码实现拆解:从数据准备到结果输出的完整链路
4.1 工程文件结构设计
我的升级优化版代码文件组织如下:
00_InitData.m % 参数与数据初始化 01_BuildUncertaintySet.m % 构建不确定集 02_MasterProblem.m % 主问题建模与求解 03_SubProblem.m % 子问题建模与求解 04_CCG_Main.m % C&CG主循环 05_PlotResults.m % 结果可视化这个结构可以让主程序很薄,调试时也能单独验证每个模块,不用每次跑全流程。数据初始化文件里包含分时电价曲线、光伏预测值和预测偏差、负荷预测、MT机组参数、储能参数、PCC联络线参数。
4.2 主函数C&CG循环的代码逻辑
主循环代码逻辑大致是这样:
% 04_CCG_Main.m 核心逻辑 LB = -1e8; UB = 1e8; gap = 1; k = 0; K_max = 30; scenarios = u_forecast; % 初始场景取预测值 while gap > 0.01 && k < K_max k = k + 1; % 求解主问题 [x_mp, eta, status_m] = MasterProblem(scenarios); LB = max(LB, FirstStageCost(x_mp) + eta); % 固定x_mp,求解子问题 [Q, u_worst] = SubProblem(x_mp); UB = min(UB, FirstStageCost(x_mp) + Q); % 计算相对间隙 gap = (UB - LB) / abs(UB); fprintf('Iter %d: LB=%.2f, UB=%.2f, gap=%.4f\n', k, LB, UB, gap); % 把最恶劣场景加入主问题的场景集 scenarios = [scenarios, u_worst]; end这里有几个细节要注意。初始场景取预测值,保证了主问题在第一轮就有一个可用的可行解。LB的更新用的是主问题目标函数值,不过第一轮主问题里η可能还没有任何对应的第二阶段约束,需要单独处理。我一般采用从UB=1e8、LB=-1e8初始化的方式,让第一轮迭代一定可以进去。
4.3 主问题建模的关键Yalmip片段
主问题函数内部,用Yalmip建变量时特别要注意场景索引。每种场景需要一套独立的第二阶段变量:
% 02_MasterProblem.m 关键片段 p_mt = sdpvar(N_T, 1); % MT基准出力 p_buy = sdpvar(N_T, 1); % 购电功率 p_sell = sdpvar(N_T, 1); % 售电功率 u_mt = binvar(N_T, 1); % MT启停状态 soc = sdpvar(N_T, 1); % 储能SOC p_ch = sdpvar(N_T, 1); % 储能充电功率 p_dis = sdpvar(N_T, 1); % 储能放电功率 % 每个场景的第二阶段变量 for s = 1:length(scenarios) delta_mt{s} = sdpvar(N_T, 1); delta_buy{s} = sdpvar(N_T, 1); delta_sell{s}= sdpvar(N_T, 1); curtail{s} = sdpvar(N_T, 1); % 弃光 load_shed{s} = sdpvar(N_T, 1); % 切负荷 p_ch_adj{s} = sdpvar(N_T, 1); p_dis_adj{s} = sdpvar(N_T, 1); end第一阶段的成本包括MT燃料成本(通常用线性化分段函数近似)、购电成本、储能充放电损耗成本。第二阶段的成本是各场景下调整量的惩罚费用,包括弃光惩罚、切负荷惩罚和功率调整惩罚。
主问题约束要在基本约束基础上,加上“交通灯”式的场景耦合。每个场景下必须满足该场景的功率平衡等式,而平衡等式里要包含第一阶段变量以及该场景对应的第二阶段调整量。这段话说起来简单,实际建模时效应最大的一个原则是:所有变量维度保持一致,矩阵运算时注意行/列方向。
4.4 子问题建模与强对偶的处理方式
子问题的内层最小化问题用Yalmip的常规方式就能建模,但整个max-min问题需要手动转化。我在代码里是用强对偶方式处理的:先把内层min写成紧凑形式,推导出对偶问题,再把对偶目标和不确定变量u的乘积项用大M法线性化。
核心对偶约束要正确定义。对偶变量维度必须与原始约束数量一一对应,尤其要区分等式约束(对偶变量自由)和不等式约束(对偶变量非负)。这一步如果错了,子问题优化结果会完全随机,而且表面上看不出异常,只能拿小规模算例手算检验。
子问题求解完成之后,不仅要返回Q值,还要返回最恶劣场景u_worst。这个场景向量是后续加入主问题的新增量,也是C&CG“列生成”的核心。
4.5 结果可视化与调度方案解读
结果图我习惯画四张子图:第一张是C&CG的LB/UB收敛曲线,第二张是各时段光伏、负荷的预测值与最恶劣场景值对比,第三张是MT出力、储能充放电、购售电功率的功率平衡图,第四张是SOC曲线。
看调度结果时重点看两个逻辑对不对。分时电价谷段(比如23:00到次日7:00)是否出现明显的购电和储能充电行为,峰段(比如10:00-14:00)是否会放电替代购电,这是验证模型经济性的直观方式。另外一个必查项是SOC曲线是否始终保持在上下限内,以及终点SOC是否回落到初始值的允许偏差内,这能快速排查储能约束是否写漏。
5. 升级优化版本改进了什么:四个提升点
5.1 储能精细化建模:充放电效率不对称与寿命折损
基础版储能建模通常写成两个不等式加一个SOC递推公式,假设充放电效率相同,且完全不考虑循环寿命。升级版把充放电效率分开处理,充电效率和放电效率分别取值(比如0.95和0.92),SOC递推公式也随之区分。同时加入运行维护成本项,按充放电电量线性折算,避免储能“过度使用”。
这个改进直接影响调度策略:基础版里储能可能会被频繁地“小循环”充放,看起来每次都有收益,但累积的寿命成本被忽略了。加了这个成本项之后,调度结果中储能的每日循环次数会明显下降,实际运行更合理。
5.2 分时电价下的电网交互策略:购售电价分段与功率平滑
基础版里购售电价往往取固定峰值、谷值两段,升级版把每个时段的购电价和售电价都做成阶梯式,更接近真实市场的分时电价结构。同时在PCC交互功率上增加了平滑约束,限制相邻时段购电功率的变化率,避免调度方案出现“买电—卖电—买电”的振荡行为。
这种振荡在纯经济优化里确实可能出现,因为只要峰谷价差大于损耗成本,模型就会尝试来回套利。加入变化率限制后,调度方案在工程上可执行性更强。
5.3 滚动时域机制:把两阶段鲁棒嵌入动态调度
升级版最重要的结构变化是把两阶段鲁棒优化放进滚动时域(RHC)框架里执行。调度过程分两个层面:外层按小时滚动,每个决策时点重新采集最新光伏预测和负荷预测,更新不确定集参数;内层对剩余调度时域跑一次两阶段鲁棒优化,但只执行当前时段的第一步决策。
这样做的好处有两个:预测信息越临近越准确,滚动更新能显著降低不确定性范围;最坏场景的“反击”强度会随时间衰减,整体调度成本比离线一次性优化的结果更低。代价是求解次数变多了:24小时调度,如果每15分钟滚动一次,需要求解96次两阶段鲁棒模型。升级版在算法层做了加速,否则计算时间会很难看。
5.4 求解加速手段:多切割C&CG与MIP gap控制
滚动时域带来的计算压力,促使我在C&CG层做了两个加速优化。一是多切割C&CG,在子问题求解时一次性识别多个局部恶劣场景,而不是每轮只加一个场景,用一个不太标准的说法就是“一步到位把最坏的方向都试探出来”。二是在主问题求解时把相对MIP gap设为0.5%而不是默认的0.01%,日用调度场景下这个精度损失几乎无感知,但求解时间能下降一半以上。
经过加速后,单个时段的C&CG迭代次数通常在5-12次之间收敛,单次求解时间在十几秒到一分钟左右,整个滚动时域流程可以在半小时内完成。对照基础版,同样的滚动流程可能需要数小时,这种差距在实际项目里非常致命。
6. 反复踩坑后总结的五个调试经验
6.1 大M取值不当引发的“伪最优解”
双线性项线性化需要一个足够大的常数M。我最早图省事设成1e6,结果求解器返回的“最优解”检查时发现某些辅助变量取值完全偏离物理量纲。原因是大M过大会导致数值条件数变差,MILP求解时会出现数值不稳定。
经验做法是先估算目标函数量级,比如单时段成本在几百元量级,大M取1e3到1e4就够了,不需要更大。太小的M又会截断可行域,所以最好用预测场景和极端场景交叉验证,确保M没有卡住任何物理可行的场景。
6.2 子问题对偶约束写错,但表面看起来“一切正常”
有一次子问题返回的目标值异常偏高,最开始怀疑是不确定集参数设置问题,排查了两天才发现是对偶变量的符号约束写反了。原始的不等式约束对应非负对偶变量,等式约束对应自由变量,如果从Yalmip输出的约束顺序去数对偶变量位置,很容易数错。
推荐做法是先用一个极小算例做手算验证。比如3个时段、1个不确定参数,预先把子问题的最优值推导出来,拿代码结果对比,确认无误再扩展到完整24时段模型。这一步看似费时,实际是排查高级问题的基础。
6.3 主问题场景无限增长,求解时间指数级上升
C&CG每轮都会增加一组场景变量和约束,迭代到后期主问题规模会非常大,十几轮之后可能明显变慢。观察下来,一个30个时段、20个迭代轮次的模型,主问题变量数能达到数千个,Yalmip建模时间也会显著变长。
缓解手段有三个:一是适当放宽收敛阈值,gap取2%左右可以减少一两轮迭代;二是每求解完主问题后,把已经添加的场景检查一遍,去掉作用很小、权重接近于零的场景;三是在主问题MIP求解时限制节点的搜索策略,优先用分支定界默认策略也就是加一个MIP gap限制,通常能阻止最坏的情况。
6.4 储能SOC约束“自洽但错误”:终点SOC没回到初值
基础版里如果只强制SOC在每个时段上下限内、不限制终点时段SOC,模型会把储能用到极值状态来降低总成本,得到的“最优方案”实际上不可持续。比如第一天把电放光,第二天就无法继续调度了。
升级版里我用一个软约束加惩罚项处理:允许终点SOC偏离初始值,但在目标函数里加偏离惩罚,偏离单位成本的惩罚设置得比购电价格高一些。这样既能给模型灵活性,又能保证长期运行的可持续性,比硬性相等约束效果更好。
6.5 不确定度参数Γ的灵敏度测试不能省
很多朋友会把Γ直接取一个固定值,比如6或8,然后就开始分析结果,但Γ的取值对调度成本的影响是高度非线性的。我在调试时会把Γ从0扫描到T,画出“成本–保守度曲线”,你会发现成本曲线一般存在一个明显的拐点,拐点之后成本急剧上升但鲁棒性提升有限。
这个拐点对应的Γ值就是实际调度中比较合理的设置。用这个方法做参数选取,比拍脑袋定参数更有说服力,也能在汇报方案时直观展示“为什么选这个保守度”。
结尾
这套代码和模型我从基础版迭代到升级优化版,最大的体会是:两阶段鲁棒优化的难点不在建模本身,而在怎么让模型在真实数据下跑得稳、算得快、参数选得准。尤其是滚动时域和C&CG结合之后,问题规模翻倍,每一层都可能出错,但只要保证主问题、子问题、参数更新这三块各自正确,整个系统就会变得非常可靠。
如果你要在自己的微电网项目里用这套方法,我的建议是从一个最简版的盒式不确定集和单场景模型开始,先跑通一版结果,再逐步加入预算约束、滚动时域、储能精细化建模。每加一块就重新验证一次结果,这样出问题时定位也快。
另外一个小建议是:拿到任何新数据,先跑一遍Γ=0的确定性情况,作为基准值和代码正确性的参照。再做Γ=3、5、8的扫描,能直观看到鲁棒性代价是什么样的。这套调试流程能帮你避开绝大多数“看起来能跑、跑出来却是错”的尴尬。