
1. 为什么虚拟电厂日前调度最怕“源-荷双重不确定性”做虚拟电厂调度的人应该都有这种感觉白天看光伏预测曲线还挺漂亮晚上看负荷预测也很平稳但真到了实际运行那一天光伏出力忽高忽低、负荷峰谷来回漂移按预测值做的日前计划往往被现实打得面目全非。1.1 虚拟电厂内部到底有哪些可调资源虚拟电厂不是一座真正的电厂而是一个通过协调控制平台把分散的分布式光伏、分散式风电、储能系统、燃气轮机/柴油发电机、柔性负荷、电动汽车充电桩等资源聚合成一个整体对外参与电力市场或接受电网调度的系统。从调度模型的角度看虚拟电厂本质上是一个“多能源设备的聚合优化问题”和传统微网的优化调度在数学形式上高度相似。我们做日前调度时输入的边界条件一般包括光伏和风电的日前预测出力曲线、负荷预测曲线、各设备的运行参数、与上级电网的交互电价分时电价/实时电价。决策变量则包括各机组在各时段的出力、储能系统的充放电功率和SOC状态、可中断负荷的削减量、与电网的购售电功率等。这里面的麻烦在于虚拟电厂的可调资源多而杂设备特性差异很大。储能是“双刃剑”——既能充电也能放电但SOC状态会跨时段耦合燃气轮机爬坡速率快但有启停成本和最小出力限制柔性负荷虽然响应灵活但用户舒适度和削减量的关系很难精确建模。把这些约束都写进一个混合整数线性规划MILP模型里问题规模就已经不小了再叠加上不确定性模型复杂度直接翻倍。1.2 确定性日前计划在极端场景下的翻车现场大多数刚接触这个方向的人第一步做的都是确定性调度把光伏出力、风电出力、负荷都取预测期望值然后求解一个确定性的MILP得到一组最优日前计划。这样做在预测误差很小的时候问题不大但一到强对流天气、云层快速移动、或者局部负荷突增的场景问题就暴露了。举个例子。假设某天光伏预测出力是100MW实际因为云层遮挡只有60MW这时候差额40MW需要由储能、燃气轮机或者上级电网来补。如果日前计划里已经让燃气轮机满发、储能SOC安排得很紧凑、购电计划也卡在上限附近那实时阶段就会面临三个选择要么切负荷最坏的情况要么临时高价购电成本剧增要么紧急调高燃气轮机出力可能超出爬坡约束做不到。确定性模型的本质缺陷在于“把所有预测值当成事实”。我们在实际工程项目里复盘过多次碰到的真实情况往往比上述场景更复杂——光伏和负荷同时出现不利偏差比如上午光伏骤减的同时空调负荷猛增。这种“双重度”的不利情形在确定性模型里完全没有被考虑结果就是日前计划在极端场景下的失配率高得惊人。1.3 鲁棒优化为什么比随机规划更贴近工程习惯处理不确定性有两个主流数学工具随机规划和鲁棒优化。随机规划的思路是给定不确定参数的概率分布然后用蒙特卡洛采样或场景缩减技术生成若干代表性场景求解一个“所有场景下期望成本最优”的二阶段模型。它的优点是经济性更好不会过度保守缺点是对概率分布的准确性要求很高而且场景数量一大计算量迅速膨胀。鲁棒优化的思路则完全不同它不依赖概率分布而是假设不确定参数落在某个“不确定集合”内优化目标是“即使在最坏情况下方案也能满足约束且成本尽可能低”。这就很像工程上常用的“留裕度”思路——不赌概率而是把最坏情况先框住再求可行解。对工程实践来说鲁棒优化有两个天然优势。第一预测误差区间的获取比概率分布容易得多——运维人员看历史数据就能拍出一个大致范围但要让分布式光伏的出力误差服从某个具体分布其实挺难的。第二鲁棒优化解出的计划自带“抗风险基因”实时调度阶段的调整压力会小很多。当然鲁棒优化的代价就是解出来的成本会比确定性模型高一些这个“高出来的成本”本质上就是买保险的保费。问题在于怎么控制这个“保费”的额度——这就要靠不确定集合的设计来调节保守度了我们下面详细拆解。2. 源-荷双重不确定性的数学化盒式不确定集与两阶段鲁棒框架2.1 源侧不确定集光伏/风电预测误差的区间描述把光伏出力、风电出力写进鲁棒模型的第一步是把它们从确定的预测值改成“预测值±偏差区间”的形式。业内最常用的就是盒式不确定集box uncertainty set表达如下光伏出力的实际值 P_pv(t) 落在预测值 P_pv_pre(t) 加减最大偏差 ΔP_pv(t) 的区间内风电出力同理P_wt(t) 落在 [P_wt_pre(t) - ΔP_wt(t), P_wt_pre(t) ΔP_wt(t)] 内负荷的实际值也类似在预测值附近波动。数学上写作 P_pv(t) ∈ { P_pv_pre(t) ξ_pv(t) · ΔP_pv(t) }其中 ξ(t) ∈ [-1, 1] 是归一化扰动系数。但直接把这个区间用到模型里会出现一个很严重的问题——所有不确定参数都同时取到边界值。实际上光伏出力不可能全天24小时都偏差到最大负向负荷也不可能24小时都偏差到最大正向。如果模型假设每个时段都发生最坏偏差那么解出来的调度计划会保守得离谱峰谷时段几乎全在“为不可能发生的场景留后手”。因此需要引入**预算参数budget**来控制不确定性的“总强度”。比如设定全天各时段的光伏总偏差强度不超过 Γ_pv数学上写成 Σ|ξ_pv(t)| ≤ Γ_pv。这样不确定集合就变成了“盒式集合 预算约束”的组合这是目前工程上最常用、也最容易在MATLAB里落地的形式。另一个常见做法是引入置信度水平。把预测误差的分布按历史数据统计出90%或95%分位数作为偏差区间的边界。这样选择的区间既不会过宽导致保守又能覆盖绝大多数实际场景。我们在实际项目中一般取90%分位数在成本和可靠性之间取一个比较平衡的点。2.2 荷侧不确定集负荷波动与柔性负荷的调节空间负荷侧的建模要稍微复杂一些。因为虚拟电厂里的负荷并不完全是“刚性”的有相当一部分是可中断负荷或可转移负荷这部分本身就有调节能力。在鲁棒优化框架下我会把负荷分成两个部分来处理第一部分是刚性负荷这部分必须满足只能靠其他电源去平衡不确定性体现在它的预测误差上实际负荷落在预测值加减偏差区间内。第二部分是柔性负荷包括可中断负荷、可平移负荷、温控负荷等。它们的功率本身就是决策变量可以在一定范围内被削减或转移这相当于给了调度一个“安全阀”。在模型中通常把柔性负荷的响应量写成一个带约束的决策变量比如可削减比例上限、可转移时段的限制等。这部分做得越精细模型与实际运行的贴合度就越高鲁棒解的“保费”也越低。还有一个细节值得注意——源和荷的不确定性并不完全独立。光伏出力大幅下降的天气往往伴随着气温变化进而影响负荷。虽然准确的联合分布很难刻画但在模型里我们可以通过共享同一个扰动场景的做法来近似反映这种相关性。最简单的方式是在生成不确定场景时让光伏偏差和负荷偏差的符号方向一致都取正或都取负。我在实测中发现这个设定对解的质量影响还挺明显的。2.3 两阶段鲁棒模型的min-max-min结构与决策时序理解了不确定集合之后就可以把整个调度问题构造成典型的两阶段鲁棒优化模型外层min是第一阶段日前计划的决策在不确定参数尚未观测到时做出确定机组启停、储能充放电计划、购售电计划等“事前决策”变量。中层max是“大自然/最坏情况”的选择在不确定集合内找到一组使运行成本最高的光伏/风电/负荷出力组合。这是鲁棒优化区别于确定性优化的关键——它显式地求解最坏场景。内层min是第二阶段实时调整的决策在最坏场景确定后通过调整机组出力、储能功率、负荷削减等“事后调整”变量以最低的调整成本满足功率平衡。整个模型的目标函数是第一阶段成本加上第二阶段最坏情况下的调整成本约束条件则包含设备运行约束、功率平衡约束、不确定参数属于不确定集合等。这个结构用数学符号表达就是min_x { cx max_u min_y dy }这里 x 是第一阶段决策u 是不确定参数y 是第二阶段决策。约束条件必须对集合内所有 u 都成立。这类模型求解的核心方法是列与约束生成CCG算法我们下一步讲代码怎么落地。3. MATLAB代码落地从约束建模到CCG迭代求解3.1 环境配置与变量定义YALMIPCplex的最短路径MATLAB里做鲁棒优化的捷径是用YALMIP工具箱建模再调用商用求解器求解。我在多个项目里用的组合是YALMIP Cplex或者YALMIP Gurobi。前者胜在许可证好获取后者胜在某些MILP场景下求解更快两者在YALMIP框架下的代码写法几乎一致。安装环节有个容易被忽略的点YALMIP对MATLAB版本有一定要求新版YALMIP需要R2018b以上才能跑得顺老版本在求解一些含二阶锥约束的模型时容易报“无法识别”的错误。装好之后先用yalmiptest命令跑一遍自检确认求解器能被正确识别这一步能省掉后面很多莫名其妙的报错。定义变量的核心代码如下% 时段数 T 24; % 第一阶段变量 x_pg binvar(1, T); % 燃气轮机启停状态 x_pg_out sdpvar(1, T); % 燃气轮机出力 x_soc sdpvar(1, T); % 储能SOC x_ch binvar(1, T); % 储能充电状态 x_dis binvar(1, T); % 储能放电状态 x_buy sdpvar(1, T); % 购电功率 x_sell sdpvar(1, T); % 售电功率 % 不确定参数源、荷 u_pv sdpvar(1, T); % 光伏实际出力 u_wt sdpvar(1, T); % 风电实际出力 u_load sdpvar(1, T); % 负荷实际值 % 第二阶段调整变量 y_pg_adj sdpvar(1, T); % 燃气轮机调整量 y_ch_adj sdpvar(1, T); % 储能充电调整量 y_dis_adj sdpvar(1, T); % 储能放电调整量 y_curtail sdpvar(1, T); % 负荷削减量注意binvar定义的是0-1整数变量在MILP中这些二进制变量用来描述机组启停、储能充放电状态互斥等非线性逻辑。储能充放电状态互斥约束要写成 x_ch x_dis 1否则求解器可能会给出“同一时段既充电又放电”的荒谬结果。3.2 主问题MP的MATLAB实现CCG算法把两阶段鲁棒问题拆成主问题Master Problem, MP和子问题Subproblem, SP交替迭代求解。主问题的形式是在“已知的若干不确定场景集合”下求解第一阶段决策和第二阶段调整成本之和的最小值。每次迭代会把子问题找出的最坏场景加入主问题的场景集合相当于不断“增长”主问题需要考虑的恶劣情况。主问题在YALMIP中的约束组装大概是这样的Constraints []; for t 1:T % 机组出力上下限取决于启停状态 Constraints [Constraints, x_pg_min * x_pg(t) x_pg_out(t) x_pg_max * x_pg(t)]; % 储能SOC递推 Constraints [Constraints, x_soc(t1) x_soc(t) eta_ch * x_ch_adj(t) - x_dis_adj(t) / eta_dis]; % 储能容量约束 Constraints [Constraints, soc_min x_soc(t1) soc_max]; % 与电网交互功率限值 Constraints [Constraints, 0 x_buy(t) buy_max]; Constraints [Constraints, 0 x_sell(t) sell_max]; end % 功率平衡约束每个不确定场景下都要满足 for k 1:iter_count % iter_count为当前场景数 Constraints [Constraints, ... x_pg_out(t) u_pv_k(t) u_wt_k(t) x_dis_adj(t) - x_ch_adj(t) x_buy(t) ... u_load_k(t) x_sell(t) - y_curtail_k(t)]; end场景下标 k 是迭代产生的“最坏场景”编号每轮CCG迭代后会在约束里追加一个新场景的平衡方程。目标函数写为Objective sum(成本项) sum(调整成本项); ops sdpsettings(solver, cplex, verbose, 0); optimize(Constraints, Objective, ops);这里有一个关键技巧主问题不要一开始就把所有可能场景都加进去而是让CCG迭代“按需”加入否则主问题规模会大到求解器难以承受。3.3 子问题SP的对偶转换与big-M处理子问题解决的是“给定第一阶段决策后在最坏不确定性下最小化调整成本”的问题即 max_u min_y 部分。内层的 min_y 是线性规划可以直接通过拉格朗日对偶转化成 max 问题与原外层的 max 合并子问题就被改写为单一的最大化问题。但这个转化过程中会出现“双线性项”——不确定性变量 u 和对偶变量 π 相乘的项这会让问题变成非凸的。解决标准做法是用big-M法把双线性项线性化。引入辅助变量替换 u·π同时用M值约束辅助变量的取值范围。M值的选取需要大于对偶变量的理论上界实际中需要反复测试调整选得太小会切掉可行域选得太大又会影响数值稳定性。子问题求解得到的对偶目标函数值就是“最坏场景下的调整成本”同时最优解对应一组 u*也就是当前最需要被纳入主问题的最坏场景。3.4 CCG主循环的收敛判据与迭代流程CCG主循环的完整流程如下初始化设定初始场景集合一般取不确定参数的预测值作为第一个场景设置迭代次数 iter 1。求解主问题得到第一阶段决策 x^* 和目标函数下界 LB。固定第一阶段决策求解子问题得到最坏场景 u^* 和目标函数上界 UB。如果 (UB - LB) / UB 小于设定的收敛阈值一般取0.01或更小停止迭代输出结果。否则把子问题找到的最坏场景 u^* 作为新场景加入主问题的场景集合iter iter 1回到第2步。这段逻辑在MATLAB里可以用while循环实现代码本身并不复杂但有个细节要注意子问题每次求解完都要检查可行性如果出现不可行需要加入相应的可行性割平面而不是直接加最优性割平面。这个分支处理在CCG里很容易被忽略但对算法的收敛性影响很大。while abs(UB - LB) / abs(UB) 1e-3 % 求解主问题 optimize(MP_Constraints, MP_Objective, ops); LB value(MP_Objective); x_current value([x_pg_out; x_soc; x_ch; x_dis; x_buy; x_sell]); % 固定x_current求解子问题 optimize(SP_Constraints, SP_Objective, ops); UB min(UB, value(SP_Objective) fixed_cost); worst_scene value(u_pv); % 将worst_scene加入主问题 MP_Constraints [MP_Constraints, balance_cons(worst_scene)]; end实际跑下来一般迭代4-10轮就能收敛到不错的精度。如果超过15轮还不收敛多半是子问题对偶或big-M参数出了问题下面第5节详细说。4. 算例设计、参数标定与结果解读4.1 测试系统的资源参数与不确定集参数为了验证模型和代码的正确性我们搭建了一个典型的中型虚拟电厂测试算例所有参数如下表所示。设备类型关键参数数值分布式光伏装机容量200 MW分散式风电装机容量100 MW燃气轮机出力范围20–100 MW燃气轮机爬坡速率30 MW/h储能系统容量50 MW / 100 MWh储能系统充放电效率0.95储能系统最大充放电功率25 MW刚性负荷峰值180 MW柔性负荷可削减比例10%上级电网最大购电功率120 MW光伏、风电、负荷的日前预测取自典型夏季日曲线。不确定集合的参数设置如下光伏最大偏差比例为预测值的±20%预算参数 Γ_pv 624个时段中最多允许6个时段同时达到最大偏差风电最大偏差比例为预测值的±25%预算参数 Γ_wt 6负荷最大偏差比例为预测值的±5%预算参数 Γ_load 8。这个参数组合的意思是说我们承认一天中会有若干个时段出现较大偏差但这些“坏时段”的总数受到限制不会24小时都坏到极点。预算参数越大解越保守预算参数越小解越激进。实际工程中可以用历史数据滚动校准这些参数。4.2 三组对照实验确定性、弱鲁棒、强鲁棒为了说明鲁棒模型的实际效果我们跑了三组对照第一组是常规确定性模型光伏、风电、负荷都取预测值不做任何不确定性处理。第二组是弱鲁棒模型预算参数取上面的默认值保守度中等。第三组是强鲁棒模型把预算参数调大各时段的不确定区间都扩大一倍预算参数翻倍代表极端保守。三组模型的日前运行成本包括购电成本、机组燃料成本、储能损耗成本、削减补偿成本如下模型日前计划成本万元最坏场景下实际成本万元成本偏差率确定性模型42.658.336.9%弱鲁棒模型45.148.26.9%强鲁棒模型49.850.41.2%可以看到确定性模型算出来的日前计划成本最低但一旦遭遇最坏场景实际成本飙升36.9%远超出预算。弱鲁棒模型日前成本只多花了2.5万元约5.9%的“保费”但最坏场景下的成本偏差被压缩到7%以内。强鲁棒模型则几乎完全锁死了最坏情况但代价是日前成本高了16.9%。从这个对照就能理解为什么工程上推荐用中等保守度多花6%的预算换掉近30%的成本波动风险这笔“保险费”非常划算。而极端保守的模型虽然风险几乎为零但经济性太差实际运行中很难被接受。4.3 从调度曲线看鲁棒解的“预留容量”逻辑把三组模型的日前调度曲线拉出来对比能非常直观地看到鲁棒解的行为模式。确定性模型的储能策略是“低谷充电、高峰放电”把峰谷价差吃满燃气轮机在负荷高峰时满发。看起来把设备利用率和套利空间都做到了极致但代价是没有任何冗余。弱鲁棒模型的调度则出现了几个有意思的变化一是储能SOC不再“卡”在边界附近而是保留了一定的调节裕度二是在光伏预测出力较高的时段模型没有把燃气轮机压到最低出力而是保持了部分出力作为“备用”三是与电网的购电计划也没有顶到上限留出了一部分空间给实时调整。这些“刻意的浪费”正是鲁棒解对抗最坏场景的底气所在。我在给某个虚拟电厂项目的运行团队解释这套逻辑时打了个比方确定性模型是跑车追求极速但遇到坑洼会颠坏鲁棒解是SUV速度略慢但通过性好。实际调度运行中真正让人头疼的不是平均场景下的经济性而是极端场景下的“扛得住扛不住”问题鲁棒优化的价值正在于此。5. 调试过程中踩过的坑和最终工程化建议5.1 子问题对偶出现无界解双线性项的big-M线性化我在头几次跑CCG时遇到最多的问题就是子问题求解报“unbounded”或无界解。排查下来发现是双线性项线性化时big-M参数选得不对。具体来说子问题对偶化后目标里会出现 u(t) · λ(t) 这样的乘积项。我一开始拍脑袋设了 big_M 1e6结果某些约束下对偶变量的最优值远超这个数量级导致线性化后的可行域被错误地截断求解器判断模型无界。解决方法是先跑一次不带big-M的松弛版本比如把u固定为预测值观察对偶变量的数量级范围然后按这个范围的2-3倍来设big-M。这个方法虽然土但非常管用。后来有同事提醒我也可以用分段线性近似的办法来替代big-M数值稳定性更好不过实现复杂度会高一些。5.2 迭代不收敛时的加速手段初值注入与约束池清理另一个常见问题是CCG迭代次数异常偏多甚至超过30轮还在震荡。我后来总结出两个有效对策。第一个对策是给主问题注入合理初值。刚开始迭代时主问题没有任何场景支撑求出来的第一阶段变量往往质量很差导致子问题找出的最坏场景也偏激反过来又让主问题变保守形成“来回甩”的震荡。解决方式是先在主问题里加入预测场景不确定性参数取预测值作为初始场景让第一阶段变量先落在一个相对合理的区域再开始迭代。第二个对策是约束池清理。迭代轮数多了之后主问题里积累了大量相似度很高的场景约束这些约束不仅不提供新的信息还拖慢求解速度。我一般每5轮迭代就检查一次如果新场景与已有场景的最大偏差小于3%就跳过不加入。这个技巧能把总迭代时间缩短一半以上。还有一个隐藏得很深的坑储能SOC的递推约束在CCG框架下要特别小心。第一阶段变量里的SOC必须始终满足“任何场景下都不能越界”的要求所以SOC的上下限约束必须在主问题的每个场景约束里都重复加入。我第一次实现时只在末尾时段检查了SOC结果子问题报出了“SOC越界”的可行性问题排查了好久才发现是这个原因。5.3 实际工程落地的几个建议最后分享几条从纯学术demo走向实际项目时积累的经验。一是不要试图把模型做得“面面俱到”。虚拟电厂内部设备类型多、特性杂如果每个设备都建立超精细模型问题规模和求解时间会失控。工程上比较务实的做法是对影响显著的大容量设备储能、燃气轮机、集中式风电光伏精细建模对小容量设备小型柴油机、微型燃气轮机等做聚合等效用一台“等效机组”代替多台小机组。二是鲁棒参数需要定期校准。我见过不少项目模型本身没问题但用了半年前标定的不确定区间结果与实际运行情况完全不符。光伏、负荷的预测误差特征随季节、天气、甚至节假日模式都会变化建议每月用滚动历史数据重新统计一次偏差区间和预算参数。三是把鲁棒优化的结果和实时调度系统打通。日前鲁棒调度算出来的是“计划”实际运行中还需要配合模型预测控制或实时经济调度来做分钟级的调整。鲁棒计划的意义在于它给出了一个足够安全的工作点实时调度在这个工作点附近做小范围调整即可不会动不动就触碰设备边界。四是注意超短期预测数据的接入。日前鲁棒模型解决了未来24小时的整体计划问题但如果能接入未来4小时的超短期预测数据可以显著缩小不确定区间从而降低鲁棒解的保守度。我在某个示范项目中就是采取了“日前鲁棒计划日内滚动修正”的两级架构实际运行效果非常理想日前计划的“保费”又压低了近三分之一。我在实际调试这套MATLAB代码时的最大体会是模型框架和分析思路对了代码实现只是时间问题真正花时间的地方永远在细节处理——边界条件的写全、big-M的调试、收敛判据的选取。这些坑每一个都值得记录下来希望这篇文章能把该绕的路都帮你绕过去让你在搭建自己的虚拟电厂鲁棒调度模型时少走几个来回。