
做微电网日前优化调度的朋友应该都有过这种体验光伏和负荷预测曲线在计划阶段画得漂漂亮亮第二天一跑实际出力和用能跟预测差出一大截。尤其是光伏一阵云飘过来功率能掉一半这种不确定性靠常规的确定性优化根本防不住。所以这两年两阶段鲁棒优化在微网优化调度里越来越受关注它不要求你给出精确的概率分布只要界定一个“不确定集合”就能在保证最坏情况系统安全运行的前提下把成本压到最低。而这类方法里最核心、也最影响求解效率的就是对“关键场景”的识别——也就是怎么从无穷多种可能的日出力组合中把最恶劣、最值得警惕的那一个找出来。这篇文章我就围绕基于关键场景辨别算法的两阶段鲁棒微网优化调度把问题模型、算法原理、Matlab实现和踩坑记录一次性讲透。1. 先搞清楚两阶段鲁棒微网调度到底解决什么问题1.1 微网调度里的确定性思维及其局限传统的日前优化调度套路很固定拿光伏、负荷的预测曲线加上系统参数建立一个混合整数线性规划或非线性规划目标是最小化运行成本约束覆盖功率平衡、机组出力上下限、爬坡约束、储能SOC约束、与主网的交互功率限制最后丢给Cplex或Gurobi一次求解。这个方案的优点是模型简单、求解快但它默认“预测值就是真实值”一旦第二天光伏少发、负荷多涨原计划里的功率平衡就被打破调度员只能临时追加调节而追加调节往往意味着更高的成本极端情况下甚至需要切负荷。很多人觉得那我把预测误差留个备用容量不就行了。但备用容量留多少是拍脑袋定的留少了扛不住大偏差留多了又在绝大多数日子里白白浪费经济性。随机规划倒是能把预测误差的概率分布考虑进去但实际工程里光伏和负荷的分布很难精确获得假设错了模型就失真了。鲁棒优化走的是另一条路不给概率只给一个包含所有可能情况的“不确定集合”然后在这个集合里找最坏情况下的最优方案。换句话说鲁棒方法牺牲一点正常天气下的经济性换来的是“无论天气怎么变系统都能安全运行”这个承诺。另外提一句这套两阶段鲁棒加关键场景辨别的思路并不只在微电网里能用。凡是“日前计划加日内修正”的调度问题都适用园区综合能源、楼宇能量管理、数据中心用能优化甚至电动车有序充电本质上都是同一个框架换一下对象和约束而已。1.2 两阶段鲁棒优化的“先决策、后调整”架构两阶段鲁棒优化的名字听起来唬人拆开看其实很符合调度员的直觉。第一阶段是“今天为明天排计划”需要提前确定哪些决策是不能等到实际出力出来之后再改的典型的比如机组启停、与主网签订的购售电协议量、储能的日前调度策略。第二阶段是“明天到来、不确定性暴露之后”光伏和负荷的实际值都知道了系统在已有的第一阶段决策基础上做经济最优的实时调整比如储能多充一点还是多放一点、实际购电是增是减、是否要弃光或切负荷。数学上这个决策过程被写成min-max-min结构min_x 第一阶段成本 c^T x max_{u∈U} min_y 第二阶段成本 d^T y最外层min是“制定第一阶段计划”中间max是“自然界跟你作对挑一个最恶劣的场景”内层min是“在这个最恶劣场景下做最经济的调整”。这种嵌套结构正好对应了调度员在日前和日内两个时间尺度上的决策逻辑所以工程上接受度很高。注意这里面有个容易误解的点鲁棒优化不是把所有时段的光伏和负荷都按最坏值算那样模型会非常保守成本高到没法用。中间那层max是在不确定集合U里挑“组合起来最恶劣的场景”而这个最恶劣场景往往只出现在某些特定时段取极端值其他时段还是正常值。这正是后面关键场景辨别算法要解决的问题。1.3 不确定集合与鲁棒预算给不确定性画个圈不确定集合的构造是整个鲁棒优化的地基。最常见的是盒式集合加预算约束。拿光伏举例可以设光伏实际出力在预测值上下20%的范围内波动即P_pv,t P_pv,forecast,t ΔP_t * ξ_t ξ_t ∈ [-1, 1]其中ΔP_t是时段t的预测偏差上限比如取预测值的20%。但如果不加限制所有时段的ξ_t同时取边界值鲁棒优化会认为太阳整天都处于最差状态这显然不合理。于是再加一个预算约束Σ |ξ_t| ≤ ΓΓ就是鲁棒预算。它限制了“偏离预测值的总程度”Γ越大允许的偏离越厉害模型越保守Γ0时模型退化为完全相信预测的确定性优化。实际操作时我建议不要一上来就拍脑袋选Γ。先跑一组Γ从0到T时段数的灵敏度分析观察总成本怎么变化。正常情况下成本会随Γ先快速上升然后趋于平缓转折点附近往往就是工程上“性价比”最高的鲁棒水平。这个经验我用过很多次比按文献里的经验值直接抄要靠谱得多。2. 关键场景辨别算法怎么快速抓到“最恶劣场景”2.1 为什么不能穷举所有极端场景现在来到这篇文章的核心算法部分。两阶段鲁棒模型写出来容易真正难的是求解。问题卡在中层的max上不确定集合U如果连续要从无穷多个场景里找最恶劣的一个如果把每个时段的光伏和负荷简单分成“高、中、低”三档一天24个时段组合数量是3的48次方这种天文数字即使再快的计算机也没法穷举。但线性规划领域有一个经典结论如果整个模型是线性的最优点一定落在约束构成的可行域的极点附近。也就是说最恶劣场景大概率出现在不确定集合的“角点”上即某些时段的光伏或负荷刚好取到偏差上限或下限。关键的难点在于一天24个时段每一个时段的光伏、负荷两个不确定变量都有上下两个极值组合起来有2的48次方量级的极点仍然多到无法枚举。所以处理思路要反过来不用枚举全部极端场景而是让算法在迭代中“主动辨别”哪些场景真正值得关注。谁值得关注答案很直观对当前的第一阶段决策杀伤力最大的那个场景。找到它、把它拿进调度模型里再去检查有没有其他更恶劣的场景循环往复直到找不出更能让系统痛苦的场景为止。这个“找最恶劣场景”的过程就是关键场景辨别算法要做的事。2.2 关键场景辨别的核心思想与算法流程在两阶段鲁棒优化领域求解嵌套min-max-min问题最主流的框架是CCG列与约束生成Column-and-Constraint Generation。它的核心思想是把原问题拆成主问题和子问题通过迭代互相喂数据。主问题是一个包含部分已知关键场景的优化模型子问题是在固定第一阶段决策后去辨别新的最恶劣场景。完整流程可以概括成四步初始化给出一个初始的关键场景集合这个集合可以来自确定性预测场景也可以来自历史典型恶劣日甚至可以只放一个场景。求解主问题在当前场景集合下同时优化第一阶段决策和所有场景对应的第二阶段成本得到一个“候选决策”和下界。固定第一阶段决策求解子问题子问题就是在不确定集合里寻找最恶劣场景以及该场景下的最小调整成本。这个场景和成本一起构成了对当前决策的“压力测试”。收敛判断与迭代把上一步得到的最恶劣场景加入主问题的场景集合重新求解主问题。当主问题的下界和子问题上界之间的间隙小于设定容差时算法收敛。每一步产生的“最恶劣场景”就是题目里说的关键场景。每迭代一次主问题里就多一个关键场景和对应的约束优化结果会越来越安全直到哪怕加入再多极端场景也不再显著改变结果时说明当前计划已经在全景范围内站得住脚了。2.3 与CCG配合每次迭代只加最关键的一个约束CCG和Benders分解的思路有相似之处但关键区别在于Benders分解是在主问题里加割平面而CCG是加新的变量和约束。具体到两阶段鲁棒问题每识别出一个关键场景主问题就会新增一组与该场景对应的第二阶段变量和约束。正因为如此CCG在小规模问题上通常比Benders收敛更快。这也正好体现了“关键场景辨别”的价值。如果不用CCG常规做法是把所有可能的极端场景一次性全部塞进主问题场景一多就变成一个大得没法解的混合整数规划。CCG不一样它每次只加一个“当前最需要警惕的场景”主问题规模是缓慢增长的而场景又是真实最恶劣的所以通常迭代十次上下就能收敛求解速度快内存占用也可控。我实际跑这类微网算例的感受是用YALMIP加Cplex求解24时段、光伏加负荷两个不确定源一般迭代8到20次就能把相对间隙压到1%以内单次主问题求解时间在几秒到几十秒之间完全能满足科研和工程验证的需要。2.4 一个直观的小例子用2时段系统理解场景辨别为了把算法说透我举个极简单的小例子。假设一个只有2个时段的微网光伏偏差在每时段只有“高、低”两个状态高代表光伏少发低代表正常甚至多发。这两个时段排列组合总共4个场景场景A时段1高时段2高整体最恶劣场景B时段1高时段2低影响较小场景C时段1低时段2高影响较小场景D时段1低时段2低最温和确定性调度相当于只看了场景D完全没考虑光伏偏差。而两阶段鲁棒的思路是先以场景D做计划得到第一阶段决策然后固定决策去检查4个场景发现场景A造成的失负荷和调整成本最高场景A就是“关键场景”。把它加入主问题重新优化得到新的第一阶段决策后再回头检查所有场景发现场景A仍然是最大威胁而且成本已经不再上升于是收敛。最终计划就是“在场景A下也不至于出大问题”的方案。用这个简单例子能看出关键场景辨别本质上是一个“寻找系统软肋”的过程。系统的软肋可能随第一阶段决策的变化而转移所以必须迭代求解。实际24时段系统只是把“枚举所有组合”换成了“用数学规划高效搜出最恶劣组合”思想完全一致。3. 两阶段鲁棒微网模型的数学建模与Matlab搭建3.1 系统结构与变量定义下面进入建模部分。这里以一个典型的并网友好型微电网为例包含光伏、风电、蓄电池储能、微型燃气轮机以及与大电网的联络线交换功率。对应用2017年电工杯微电网调度赛题背景的朋友来说这个配置很有代表性很多算例都是从那里演化来的。模型涉及的变量可以按两阶段划分第一阶段决策日前确定微燃机启停状态和日前计划出力与主网的购售电计划功率储能日前充放电计划也可放在第二阶段根据建模偏好第二阶段决策不确定性实现后调整实际光伏、风电出力水平下的储能再调整量实际购售电功率修正量弃风弃光量和切负荷量第二阶段变量带着场景索引出现也就是说每个关键场景都对应一组自己的第二阶段变量。这是CCG主问题里模型规模增长的主要来源。3.2 目标函数与分时电价建模目标函数是全天运行总成本包含以下几项向主网购电的费用减去向主网售电的收益购售电价按分时电价区分峰平谷微型燃气轮机的燃料成本一般用二次函数表示优化时需要分段线性化储能充放电带来的折旧损耗成本弃风弃光惩罚和切负荷惩罚写成数学形式min Σ_t [ c_buy(t)·P_buy,t - c_sell(t)·P_sell,t C_MT(P_MT,t) C_ESS(P_ch,t, P_dis,t) M·(P_curtail,t P_shed,t) ]其中分时电价是微网调度里的核心输入。参考国内峰平谷电价的典型设置可以按下面的表格配置时段类型时段范围购电价元/kWh售电价元/kWh峰时10:00-15:00、18:00-21:001.201.00平时07:00-10:00、15:00-18:00、21:00-23:000.800.50谷时23:00-次日07:000.400.20售电价低于购电价避免系统通过低买高卖套利这是并网微网模型里的常见设置。分段线性化方面如果用YALMIP建模可以用binvar辅助变量加额外约束或者直接用implies、ismember这类工具箱函数简化但要注意这些函数会增加约束数量求解速度会变慢。更快的做法是手动写大M分段线性化我后面避坑部分会细说。3.3 关键约束建模功率平衡、储能SOC、鲁棒预算约束方面最重要的五类不能漏功率平衡约束每个时段、每个场景P_pv P_wind P_MT P_buy P_dis P_load P_ch P_sell P_shed右边是负荷加储能充电加售电左边是各电源出力加购电。切负荷量P_shed是松弛变量正常情况下应当为0它的存在是为了保证极端场景下模型仍然有可行解。储能SOC约束SOC(t1) SOC(t) η_ch·P_ch(t)·Δt/E_cap - P_dis(t)·Δt/(η_dis·E_cap)同时要加SOC上下限约束以及充放电功率上下限约束。如果允许储能同时充放电模型会跑出“边充边放”的数值假象解决办法是加二值变量互斥约束或者干脆在目标函数里给充放电都加上成本让同时充放电不经济从而自然避免。微燃机约束出力上下限、爬坡约束、最小启停时间约束如果考虑机组组合。最小启停时间在现代调度模型里很常见但会让模型从LP变成MILP求解时间明显增加。如果只是验证算法可以先不算启停时间把重点放在鲁棒迭代上。与主网交互约束购售电功率不能超过联络线容量且购售电状态互斥也往往需要0-1变量。不确定集合预算约束光伏和负荷的实际值由预测值加偏差构成偏差项的绝对值之和受鲁棒预算Γ限制。这是关键场景辨别算法搜索范围的核心约束。3.4 主问题与子问题的数学形式及转化按CCG框架两阶段鲁棒模型可以写成标准形式min_x c^T x max_{u∈U} min_{y∈Ω(x,u)} d^T y约束形式为A x ≤ b以及对于给定场景u第二阶段可行域满足B y ≤ g - C x - D u。主问题MP是包含已识别关键场景集合K的优化问题min_x η s.t. A x ≤ b η ≥ c^T x d^T y_k对每个k∈K B y_k ≤ g - C x - D u_k对每个k∈K其中η是辅助变量代表“最坏场景下的总成本”。每次迭代增加一个场景就增加一组第二阶段的变量y_k和对应约束。子问题SP是固定x后求解最恶劣场景SP(x*) max_{u∈U} min_y d^T y s.t. B y ≤ g - C x* - D u子问题本身是max-min结构内层min是线性规划。处理标准做法是对内层LP做对偶变换把min换成max得到一个单层max问题。对偶后目标函数中出现u和对偶变量λ相乘的双线性项需要用大M法把双线性项线性化将子问题转化为一个MILP交给Cplex或Gurobi求解。这正是前面说的“关键场景辨别”模块子问题的最优解u*就是在当前第一阶段决策下能被辨别出的最恶劣场景如果子问题最优值为有限说明当前决策扛得住如果值很大这个场景就会被加入主问题继续迭代。4. Matlab实现主程序流程、求解器配置与结果解读4.1 整体程序框架与数据准备Matlab实现这个模型我习惯用YALMIP建模求解器用Cplex或Gurobi两个都支持MILPGurobi在个别算例上更快一些。数据准备阶段需要整理如下内容24小时光伏预测出力曲线、负荷预测曲线以及对应的波动偏差上限微燃机参数额定容量、爬坡率、燃料成本系数、启停成本储能参数容量、最大充放电功率、充放电效率、SOC上下限、初始SOC分时电价数据峰平时段划分和价格鲁棒预算Γ的取值CCG收敛容差和最大迭代次数一个比较容易被忽略的细节是数据单位。功率用kW、时间用h、能量用kWh、电价用元/kWh这样储能SOC约束里的Δt就是1小时计算最干净。如果数据源给的是MW一定要统一换算不然量纲错乱会导致完全无法收敛。4.2 主程序伪代码与YALMIP求解细节CCG主循环的Matlab伪代码如下% 初始化 LB -1e6; UB 1e6; k 0; scenarios {}; % 关键场景集合 % scenarios{1} nominal_scenario; % 可选先加入确定性预测场景 tol 1e-3; Kmax 50; while (UB - LB) / max(1, abs(UB)) tol k Kmax % 1. 求解主问题 [x_opt, lb] solve_MP(scenarios); LB max(LB, lb); % 2. 固定x_opt求解子问题辨别关键场景 [u_hat, sp_obj] solve_SP(x_opt); % 3. 计算总成本上界 ub_temp obj_first_stage(x_opt) sp_obj; if ub_temp UB UB ub_temp; x_best x_opt; end % 4. 将关键场景加入主问题 scenarios{end1} u_hat; k k 1; endsolve_MP内部用YALMIP建模大概长这样function [x_opt, obj] solve_MP(scenarios) yalmip(clear); x binvar(N_MT, T, full); % 机组状态如果是启停问题 p_mt sdpvar(N_MT, T, full); p_buy sdpvar(1, T, full); p_sell sdpvar(1, T, full); eta sdpvar(1, 1); % ... 其他变量 Constraints []; % 第一阶段约束功率平衡、机组出力等 % 小技巧主问题的功率平衡中光伏和负荷要用预测值极端场景偏差通过新增的y变量来补偿 for k 1:length(scenarios) % 每个场景对应一组第二阶段变量 y_k p_ch_k sdpvar(1, T, full); p_dis_k sdpvar(1, T, full); p_shed_k sdpvar(1, T, full); % 加入该场景下的功率平衡、储能SOC约束 % 加入成本不等式eta c*x d*y_k end Objective eta; % 最小化最坏场景总成本 ops sdpsettings(solver, cplex, verbose, 0, mip.tolerances.mipgap, 1e-4); optimize(Constraints, Objective, ops); x_opt value(x); obj value(eta); endsolve_SP内部是把子问题转成MILP后直接求解返回最恶劣场景u_hat和子问题目标值sp_obj。这里不再展开全部代码但有一个关键提醒子问题里的大M系数不能统一设得太小也不能太大。太小会把最优解截断太大则导致数值病态Cplex经常报数值警告。我的经验是M根据变量实际量纲设置比如功率变量上限是500kW那M取500到1000就够不要为了省事全局设一个1e6。4.3 求解器配置、收敛判据与常见调参收敛判据是整个算法能否快速结束的关键。我见过很多人直接用绝对间隙UB - LB 1e-4结果在24时段问题上迭代几十次都停不下来。改用相对间隙(UB - LB)/max(1, abs(UB)) 1e-3之后通常十几次就能收敛。原因很简单优化问题规模越大目标函数绝对数值越大绝对间隙的标准相当于要求小数点后好几位完全一致现实中完全没必要。还有几个调参经验可以共享子问题如果难以收敛可以考虑限制求解时间设一个cplex.timelimit比如120秒拿一个次优解作为场景加入主问题。因为即使场景不是绝对最恶劣只要它能让主问题产生新的约束算法依然能前进只是迭代次数可能多一点。主问题每个循环都会膨胀变量名不建议直接覆盖会导致内存消耗越来越大。用yalmip(clear)或者把每次循环的模型放到子函数里变量自动清理。Gurobi求解MILP时默认线程数可能不高可以设置gurobi.Threads为4或者8算例规模大时提速明显。4.4 仿真结果怎么看调度计划、SOC曲线、收敛曲线算法跑完后要输出几类结果来验证正确性第一是日前调度计划曲线也就是第一阶段决策里的微燃机出力、购售电功率、储能充放电计划。重点看储能是否在谷时充电、峰时放电购电是否避开峰时高价这些基本规律能验证模型逻辑是否正确。第二是SOC变化曲线。正常结果中SOC应该始终维持在上下限之间且曲线平滑不会有频繁的剧烈波动。如果SOC在某个时段反复来回反弹大概率是储能互斥约束没做好或者充放电成本设置有问题。第三是CCG收敛曲线。把每次迭代的LB和UB画出来正常情况是LB逐步抬升、UB逐步下降两条线逐渐靠拢。如果看到某一方长期不动甚至出现UB小于LB的情况说明子问题或主问题有一个算错了这是检查代码逻辑最直接的信号。第四是不同鲁棒预算Γ下的总成本对比。把Γ设成0、5、10、15、24分别跑一遍能直观看到成本随鲁棒性增强的变化趋势这也是论文里最常用的一张敏感性分析图。5. 常见问题与避坑实战5.1 子问题对偶后的双线性项怎么处理这是新手最容易卡住的地方。子问题对偶之后目标函数会变成类似于Σ (a_λ b_λ·u)·λ的形式也就是不确定变量u和对偶变量λ相乘这是个双线性项Cplex不能直接处理。最常见的做法是引入一个大M把u·λ拆成新的辅助变量w再加约束把w限制为w ≤ M·zw ≥ -M·zw ≤ λ M·(1-z)w ≥ λ - M·(1-z)其中z是0-1变量用来表示u取上边界还是下边界。这套线性化几乎在所有两阶段鲁棒优化的论文里都会出现属于必须掌握的“基本功”。操作时注意三点一是u的每一个分量都要单独做线性化不能把整个向量打包处理二是M的取值最好贴合u和λ的实际量级否则数值稳定性差三是做完线性化后整个子问题变成MILP求解时间会比LP慢一个数量级但在24时段的小规模算例里完全可接受。5.2 CCG不收敛或收敛很慢怎么办不收敛的场景我遇到得不少总结下来原因主要有三类第一类是容差设置不合理建议改用相对间隙判断第二类是子问题返回的场景质量太差比如求解时间太短导致每次拿到的场景都是次优的可以通过延长子问题求解时间或给子问题加一个“必须比现有场景差到一定程度才接受”的判断来改善第三类是第一阶段决策变量和第二阶段变量耦合过强导致主问题每次调整幅度都很大UB和LB长期对不上。针对第三类情况一个非常有效的技巧是给主问题额外加一个“确定性预测场景”作为初始场景也就是scenarios集合里先放入Γ0的场景。这样主问题一开始就有了一个真实可执行的基准解UB起始值不会太空收敛速度肉眼可见地提升。5.3 储能同时充放电、SOC漂移等工程细节储能建模里最典型的坑是“同时充放电”。如果模型对充放电功率只给了上下限没有加互斥约束求解器可能给出同时充电和放电的解两者在功率平衡中抵消但目标函数里却白算了一笔损耗成本严重误导结果。解决办法有两种一是加两个0-1变量做互斥约束二是在目标函数里同时给充放电设置损耗成本使得同时充放电不经济。第二种方法省变量、求解快但需要谨慎设置成本系数不能让成本高到影响正常充放电策略。SOC漂移是另一个容易遇到的问题。如果模型没有设置调度周期末SOC回到初始值长期运行下来储能电池可能在每个调度日内逐步亏空或充满。要不要加SOC_T SOC_0这个约束取决于实际应用场景。孤立微网通常建议加保证长期可循环运行并网型微网如果对储能连续运行要求不高可以不加把灵活性留给调度模型。5.4 数值病态与求解器报错处理跑YALMIP加Cplex时常碰到数值问题警告比如“Problem is badly scaled”。原因绝大多数是变量量纲悬殊。光伏出力几百kW、价格零点几元如果目标函数里没有做归一化矩阵里既有1000又有0.3Cplex的数值容差很难同时处理。我给的建议是功率量纲统一为kW、价格统一为元/kWh目标函数系数都尽量控制在1到1000的数量级之内。如果怎么调都还有数值警告可以考虑把目标函数整体除以1000再求解最后再乘以1000还原很多情况下能压下警告。还有一种情况是子问题有时无界。出现无界说明第二阶段可行域在某些场景下是开放的比如切负荷量没有设置上限或者储能SOC没有加下限。处理办法是给所有松弛变量设一个合理上限切负荷不超过负荷总量弃风弃光不超过对应电源出力这样数学模型才真正对应物理系统的边界。6. 写在后面我的实操体会这套“两阶段鲁棒加关键场景辨别”的模型我从最初照着论文抄代码到真正跑通自己的微网算例中间踩过的坑比想象中多得多。最大的体会是鲁棒优化不是一个“一次求解、一劳永逸”的模型它更像一套防御机制你必须先想清楚自己要防什么是防光伏波动还是防负荷冲击还是两者一起防。不确定集合、鲁棒预算、惩罚系数都是围绕“防守目标”来设的目标没想清楚参数调得再精细也白搭。另外一个很实际的建议是做这类项目时不要一上来就追求最复杂的模型。把两阶段问题拆开先跑通不含不确定性的确定性模型再跑通不含CCG的静态鲁棒模型最后再加关键场景辨别迭代。每一步都对照结果检查一遍比一次写完再回头调试要省太多时间。Matlab加YALMIP的好处是建模直观调试方便三四天时间就能从一个空文件到一个能跑出合理调度结果的完整程序。后面如果你想把工作往实际工程推进可以把确定性参数扩展成多场景随机数据甚至加进日前实时联动滚动优化框架都不用大改。