
做微电网调度的朋友应该都有过这种经历按照光照预测把机组启停、储能充放电计划排得漂漂亮亮结果第二天上午十点一片云飘过来光伏出力从1000kW瞬间掉到300kW整个日前计划全部作废只能转入实时调整。这一调整购电成本上去了切负荷的惩罚成本也出来了调度员还得问你为什么“偏差这么大”。这篇内容要说的两阶段鲁棒优化经济调度方法就是专门对付这种“预测赶不上变化”的问题。它不追求用预测值做一套完美的计划而是先做日前决策第一阶段再假设不确定性在最坏方向发生在第二阶段用最小成本调整来兜底。整个求解用CCG列与约束生成算法完成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 是策略优化。每一层都有自己的约束和变量如果直接丢给通用优化器当前主流的求解器并处理不了这种三层嵌套问题所以才需要用CCG算法把它拆开迭代求解。2.2 第一阶段决策变量和约束第一阶段变量包括MT机组的启停机状态、MT的基准出力、通过PCC与外部电网的购售电基准功率、储能电池的基准充放电功率。这些变量对应的是“日前计划”需要在不确定性实现之前定下来不能事后反悔。第一阶段约束主要有几类MT出力上下限约束每个时刻的基准出力在机组额定范围内。启停状态约束同一时刻不能既启又停最小运行时间和最小停机时间约束如果考虑会额外增加大量整数变量初期代码可以先不加。爬坡约束MT相邻时段出力变化不能超过爬坡速率。PCC功率约束购电功率和售电功率不能同时为正且不能超过联络线容量。储能基准功率约束充放电功率有上限且同一时刻不允许既充又放。这些约束用Yalmip写起来比较直接难点在之后要把它和第二阶段场景耦合。2.3 第二阶段决策变量和约束第二阶段变量是在第一阶段变量 x 和不确定场景 u 都确定之后才出现的包括MT实际出力、储能实际充放电功率、弃光功率、切负荷功率、以及和电网的实际交换功率调整量。第二阶段约束中最核心的是功率平衡方程每个时段必须满足光伏实际出力 MT实际出力 储能放电功率 购电功率 本地负荷 - 切负荷量 储能充电功率 售电功率这个等式右侧或左侧会因为不确定场景里光伏出力变化而被打破第二阶段就通过调整MT出力和储能功率、必要时弃光或切负荷来重新平衡。如果第二阶段无解意味着第一阶段方案在某个场景下不可行这就是CCG里需要生成可行性割的原因。此外储能还要满足SOC动态方程也就是荷电状态随充放电功率和时间步长的演化关系。储能这部分恰恰是最容易埋坑的地方SOC上下限、充放电效率非对称、初始和终点SOC约束任何一个写得不对结果都会不合理。2.4 不确定集怎么构造盒式加预算约束两阶段鲁棒的不确定源主要是光伏出力建模时最常用盒式不确定集也就是U { u : |u_t - u_t^forecast| Δ_t, t 1, ..., T }其中 Δ_t 是每个时段的最大偏差量通常取预测值的10%-20%。但盒式集合的问题是它允许所有时段同时偏离到极端值这种场景在现实中几乎不会出现导致结果过于保守。工程上更常用的是加预算约束budget of uncertaintysum_t ( |u_t - u_t^forecast| / Δ_t ) ΓΓ就是预算参数它的取值范围是0到T。当Γ0时不确定集退化为预测单点模型退化成确定性模型当ΓT时等于允许所有时段都取极端值保守度最高。实际调参时一般从T/3到2T/3之间去试经济和鲁棒性相对平衡。3. CCG算法求解把三层优化拆成主问题和子问题的迭代3.1 为什么用CCG而不是直接解原问题三层嵌套的min-max-min结构没有任何通用求解器能直接高效求解。主流的算法有两种Benders分解和CCGColumn and Constraint Generation。CCG的核心思路是用一组“典型恶劣场景”逼近不确定集主问题在有限个场景下求最优解子问题在给定第一阶段解的情况下找当前最大威胁的场景找到之后把这个场景加进主问题继续迭代。它的收敛速度通常比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^(k1)追加进去主问题规模会逐步变大。第一轮迭代时的主问题可以不包含任何场景直接把初始场景设为预测值场景u^(0)。之后的CCG迭代会不断往主问题里添加新场景和对应的第二段变量约束。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 CCG迭代的完整流程用步骤描述整个求解流程初始化设置下界LB-∞上界UB∞迭代计数k0选初始场景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^(k1)和相关约束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 % CCG主循环 05_PlotResults.m % 结果可视化这个结构可以让主程序很薄调试时也能单独验证每个模块不用每次跑全流程。数据初始化文件里包含分时电价曲线、光伏预测值和预测偏差、负荷预测、MT机组参数、储能参数、PCC联络线参数。4.2 主函数CCG循环的代码逻辑主循环代码逻辑大致是这样% 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的更新用的是主问题目标函数值不过第一轮主问题里η可能还没有任何对应的第二阶段约束需要单独处理。我一般采用从UB1e8、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。这个场景向量是后续加入主问题的新增量也是CCG“列生成”的核心。4.5 结果可视化与调度方案解读结果图我习惯画四张子图第一张是CCG的LB/UB收敛曲线第二张是各时段光伏、负荷的预测值与最恶劣场景值对比第三张是MT出力、储能充放电、购售电功率的功率平衡图第四张是SOC曲线。看调度结果时重点看两个逻辑对不对。分时电价谷段比如23:00到次日7:00是否出现明显的购电和储能充电行为峰段比如10:00-14:00是否会放电替代购电这是验证模型经济性的直观方式。另外一个必查项是SOC曲线是否始终保持在上下限内以及终点SOC是否回落到初始值的允许偏差内这能快速排查储能约束是否写漏。5. 升级优化版本改进了什么四个提升点5.1 储能精细化建模充放电效率不对称与寿命折损基础版储能建模通常写成两个不等式加一个SOC递推公式假设充放电效率相同且完全不考虑循环寿命。升级版把充放电效率分开处理充电效率和放电效率分别取值比如0.95和0.92SOC递推公式也随之区分。同时加入运行维护成本项按充放电电量线性折算避免储能“过度使用”。这个改进直接影响调度策略基础版里储能可能会被频繁地“小循环”充放看起来每次都有收益但累积的寿命成本被忽略了。加了这个成本项之后调度结果中储能的每日循环次数会明显下降实际运行更合理。5.2 分时电价下的电网交互策略购售电价分段与功率平滑基础版里购售电价往往取固定峰值、谷值两段升级版把每个时段的购电价和售电价都做成阶梯式更接近真实市场的分时电价结构。同时在PCC交互功率上增加了平滑约束限制相邻时段购电功率的变化率避免调度方案出现“买电—卖电—买电”的振荡行为。这种振荡在纯经济优化里确实可能出现因为只要峰谷价差大于损耗成本模型就会尝试来回套利。加入变化率限制后调度方案在工程上可执行性更强。5.3 滚动时域机制把两阶段鲁棒嵌入动态调度升级版最重要的结构变化是把两阶段鲁棒优化放进滚动时域RHC框架里执行。调度过程分两个层面外层按小时滚动每个决策时点重新采集最新光伏预测和负荷预测更新不确定集参数内层对剩余调度时域跑一次两阶段鲁棒优化但只执行当前时段的第一步决策。这样做的好处有两个预测信息越临近越准确滚动更新能显著降低不确定性范围最坏场景的“反击”强度会随时间衰减整体调度成本比离线一次性优化的结果更低。代价是求解次数变多了24小时调度如果每15分钟滚动一次需要求解96次两阶段鲁棒模型。升级版在算法层做了加速否则计算时间会很难看。5.4 求解加速手段多切割CCG与MIP gap控制滚动时域带来的计算压力促使我在CCG层做了两个加速优化。一是多切割CCG在子问题求解时一次性识别多个局部恶劣场景而不是每轮只加一个场景用一个不太标准的说法就是“一步到位把最坏的方向都试探出来”。二是在主问题求解时把相对MIP gap设为0.5%而不是默认的0.01%日用调度场景下这个精度损失几乎无感知但求解时间能下降一半以上。经过加速后单个时段的CCG迭代次数通常在5-12次之间收敛单次求解时间在十几秒到一分钟左右整个滚动时域流程可以在半小时内完成。对照基础版同样的滚动流程可能需要数小时这种差距在实际项目里非常致命。6. 反复踩坑后总结的五个调试经验6.1 大M取值不当引发的“伪最优解”双线性项线性化需要一个足够大的常数M。我最早图省事设成1e6结果求解器返回的“最优解”检查时发现某些辅助变量取值完全偏离物理量纲。原因是大M过大会导致数值条件数变差MILP求解时会出现数值不稳定。经验做法是先估算目标函数量级比如单时段成本在几百元量级大M取1e3到1e4就够了不需要更大。太小的M又会截断可行域所以最好用预测场景和极端场景交叉验证确保M没有卡住任何物理可行的场景。6.2 子问题对偶约束写错但表面看起来“一切正常”有一次子问题返回的目标值异常偏高最开始怀疑是不确定集参数设置问题排查了两天才发现是对偶变量的符号约束写反了。原始的不等式约束对应非负对偶变量等式约束对应自由变量如果从Yalmip输出的约束顺序去数对偶变量位置很容易数错。推荐做法是先用一个极小算例做手算验证。比如3个时段、1个不确定参数预先把子问题的最优值推导出来拿代码结果对比确认无误再扩展到完整24时段模型。这一步看似费时实际是排查高级问题的基础。6.3 主问题场景无限增长求解时间指数级上升CCG每轮都会增加一组场景变量和约束迭代到后期主问题规模会非常大十几轮之后可能明显变慢。观察下来一个30个时段、20个迭代轮次的模型主问题变量数能达到数千个Yalmip建模时间也会显著变长。缓解手段有三个一是适当放宽收敛阈值gap取2%左右可以减少一两轮迭代二是每求解完主问题后把已经添加的场景检查一遍去掉作用很小、权重接近于零的场景三是在主问题MIP求解时限制节点的搜索策略优先用分支定界默认策略也就是加一个MIP gap限制通常能阻止最坏的情况。6.4 储能SOC约束“自洽但错误”终点SOC没回到初值基础版里如果只强制SOC在每个时段上下限内、不限制终点时段SOC模型会把储能用到极值状态来降低总成本得到的“最优方案”实际上不可持续。比如第一天把电放光第二天就无法继续调度了。升级版里我用一个软约束加惩罚项处理允许终点SOC偏离初始值但在目标函数里加偏离惩罚偏离单位成本的惩罚设置得比购电价格高一些。这样既能给模型灵活性又能保证长期运行的可持续性比硬性相等约束效果更好。6.5 不确定度参数Γ的灵敏度测试不能省很多朋友会把Γ直接取一个固定值比如6或8然后就开始分析结果但Γ的取值对调度成本的影响是高度非线性的。我在调试时会把Γ从0扫描到T画出“成本–保守度曲线”你会发现成本曲线一般存在一个明显的拐点拐点之后成本急剧上升但鲁棒性提升有限。这个拐点对应的Γ值就是实际调度中比较合理的设置。用这个方法做参数选取比拍脑袋定参数更有说服力也能在汇报方案时直观展示“为什么选这个保守度”。结尾这套代码和模型我从基础版迭代到升级优化版最大的体会是两阶段鲁棒优化的难点不在建模本身而在怎么让模型在真实数据下跑得稳、算得快、参数选得准。尤其是滚动时域和CCG结合之后问题规模翻倍每一层都可能出错但只要保证主问题、子问题、参数更新这三块各自正确整个系统就会变得非常可靠。如果你要在自己的微电网项目里用这套方法我的建议是从一个最简版的盒式不确定集和单场景模型开始先跑通一版结果再逐步加入预算约束、滚动时域、储能精细化建模。每加一块就重新验证一次结果这样出问题时定位也快。另外一个小建议是拿到任何新数据先跑一遍Γ0的确定性情况作为基准值和代码正确性的参照。再做Γ3、5、8的扫描能直观看到鲁棒性代价是什么样的。这套调试流程能帮你避开绝大多数“看起来能跑、跑出来却是错”的尴尬。