最近正好在复现“含可再生能源和储能的区域微电网最优运行”这个方向,目标很明确:用MATLAB实现一套多阶段鲁棒调度模型,把光伏、风电出力和负荷的不确定性全部纳入优化里。为什么值得折腾这件事?因为网上大量公开案例都停留在确定性优化——预测给一组确定值,优化出一份调度计划,看着曲线很漂亮,可一旦实际光伏比预测低20%,负荷高峰比预测来得早,原方案就立刻失效,甚至可能出现切负荷。鲁棒优化要解决的就是这类“预测有偏差时方案仍然可用”的问题。
这篇文章把我从模型搭建、不确定性集合设计、CCG求解框架到MATLAB复现调试的完整过程都整理出来,代码思路和避坑经验都放在里面。适合正在做微电网经济调度、储能容量配置、鲁棒优化相关课题的学生和工程师。你可以把它当成一份“从零实现多阶段鲁棒调度”的路线图,照着走就能跑出结果。
1. 从“确定性最优”到“鲁棒最优”:这层逻辑必须先打通
1.1 确定性优化为什么在实际运行中“不够用”
先说一个我在测试案例里反复遇到的场景。某天光伏预测出力在上午10点为100kW,负荷预测为80kW,确定性优化给出的方案是:光伏给负荷供电,剩余20kW给储能充电,柴油机不启动。这个安排完全正确,但问题是它建立在一个假设上——光伏在10点真的能发100kW。实际运行中云层一过,出力可能瞬间掉到65kW。如果储能此时正在充电,无法立刻反向放电,那这20kW的缺口就只能靠柴油机启动来补,而柴油机从启动到出力至少需要几分钟,这个反应时间内系统就已经出现功率失衡了。
这就是确定性优化的本质局限。它求解的是一个“在预测值上最优”的方案,一旦实际场景偏离预测值,方案就需要被打乱重来。你可能会说,那加备用容量不就行了?加备用可以,但备用怎么加、加多少,本身就是一个优化问题。如果简单地按光伏出力的最大偏差乘以一个系数来配置备用,小节拍成本会很高,而且备用分配位置可能不对——最缺电的时刻和最多波动的时段并不总是同一个时段。
鲁棒优化换个思路:我不再假设不确定参数等于某个预测值,而是假设它落在一个“不确定集合”里,模型必须保证集合内所有可能场景都可行。这样得到的方案虽然不如确定性方案在预测场景下那么经济,但它覆盖了所有可能出现的运行工况。
1.2 多阶段决策的物理含义:今天定计划,明天再调整
“多阶段鲁棒调度”里的“多阶段”,不是指时间被切成多个时段那么简单,而是指决策按信息逐渐揭开的顺序分成多个层级。在微电网调度的典型场景中:
- 日前阶段:根据明天0:00到24:00的预测数据,决定机组的启停状态、储能的充放电基准计划、与主网的购售电计划。这一阶段的信息只有预测,不确定性尚未实现。
- 日内/实时阶段:实际光伏出力、负荷值逐渐揭晓,系统可以在日前计划的框架内调整连续变量——储能实际充多少、柴油机实际发多少、购电多少,来应对预测偏差。
多阶段鲁棒优化的核心在于,第二阶段的可调变量会根据实际看到的不确定参数取值做出“响应调节”。也就是说,不必为最坏情况锁定所有变量的值,而是允许在计划执行时通过储能、机组出力等可调变量去吸收波动。这种“可调整鲁棒优化”比单阶段鲁棒模型经济性更好,因为单阶段鲁棒要求所有决策都在不确定实现之前定死,系统会很僵硬,成本很高。
如果把单阶段比作“制定一个永远不会变的PPT计划”,那多阶段就是“今天先定大方向,明天早上根据天气微调具体动作”——后者显然更符合调度员的真实工作方式。
1.3 不确定集合与预算参数:鲁棒性的旋钮在哪
鲁棒优化模型里最关键的设计是不确定集合。我使用的最常见形式是盒式集合加上预算约束。设第t时段光伏预测出力为 ( P_{pv,t}^0 ),最大偏差为 ( \Delta P_{pv,t} ),则:
[ P_{pv,t}^U = P_{pv,t}^0 + z_{pv,t} \cdot \Delta P_{pv,t},\quad |z_{pv,t}| \le 1 ]
这里 ( z_{pv,t} ) 是一个取值在[-1,1]的连续变量,代表预测误差的归一化大小。同理,风电和负荷也适用同样的结构。但是,如果允许每个时段的误差都同时取到边界值,得到的场景会极端到现实几乎不可能发生——那种“所有时段、所有设备预测全错到离谱”的场景,其代价会被严重高估,优化结果会过于保守。
所以需要引入预算参数Γ,限制所有不确定变量偏差绝对值之和的上限:
[ \sum_t \left( |z_{pv,t}| + |z_{w,t}| + |z_{l,t}| \right) \le \Gamma ]
这个Γ就是鲁棒性和经济性之间的旋钮。Γ=0时,所有不确定参数都被锁定在预测值,模型退化为确定性优化;Γ越大,允许更多时段同时出现大偏差,系统必须预留更多调节能力,总成本也会上升。实际复现时建议扫码式地测试Γ=0、1、2、3……观察总成本的变化趋势,这样你对模型“鲁棒性的价格”有非常直观的感受。
1.4 直观案例:两个时段的偏差博弈
举个极简例子帮助理解。某微网中光伏在10点和11点的预测出力都是100kW,预测误差是±20kW。如果Γ=1,那么最恶劣场景要么是10点光伏=80kW、11点=100kW,要么是10点=100kW、11点=80kW——只有一个时段允许出现最大偏差。此时系统只需要准备20kW的额外调节能力。如果Γ=2,最恶劣场景变为两个时段光伏都等于80kW,系统需要准备40kW的调节能力。两者所需备用差异很大,成本自然也不同。这个例子虽然简单,却精准说明了预算参数的意义。
2. 模型搭建:目标函数、约束条件与储能逻辑
2.1 目标函数:怎么把“经济性”和“鲁棒性”写成一个式子
模型的优化目标是最小化总运行成本。我在代码里采用了逐时段累加的形式,包含以下成本项:
[ \min \sum_{t=1}^{T} \left( f_{dg}(P_{dg,t}) + C_{grid,t}^{buy} P_{buy,t} - C_{grid,t}^{sell} P_{sell,t} + C_{ess} (P_{ch,t}+P_{dis,t}) + C_{start} u_{start,t} \right) ]
每项的含义:
- 柴油机发电成本:用二次函数或分段线性函数表示,如 ( f_{dg}(P) = aP^2 + bP + c )。在MILP模型中,二次项通常用分段线性化处理。
- 与主网交互成本:购买电价和售电价可以不同,同一个时段内不能同时买卖,否则会出现套利循环。
- 储能运行成本:储能充放电会有损耗和寿命折损,加一个较小的单位成本系数可以让优化结果避免无意义的储能动作。
- 启动成本:柴油机冷启动需要额外代价,这个成本项直接影响日前机组启停决策——能不停就不停,能少启就少启。
注意,在多阶段鲁棒框架下,目标函数还有一层更具体的结构:第一阶段目标(主要成本)加上第二阶段的最坏情况成本(惩罚项)。但在CCG实现中,MP和SP对目标函数的处理不一样,下一节会细讲。
2.2 功率平衡与储能约束:所有约束必须逐条写清楚
模型的约束条件里最基本的是各时段功率平衡。在任意不确定场景下,系统功率平衡需满足:
[ P_{pv,t} + P_{w,t} + P_{dg,t} + P_{dis,t} + P_{buy,t} = P_{load,t} + P_{ch,t} + P_{sell,t} ]
左边是电源侧,右边是负荷和储。储能就像一个大“水池”,它同时出现在等式两边,充是负荷,放是电源。
储能系统本身的约束包括:
[ SOC_{min} \le SOC_t \le SOC_{max} ]
[ 0 \le P_{ch,t} \le u_{ch,t} P_{ch}^{max},\quad 0 \le P_{dis,t} \le u_{dis,t} P_{dis}^{max} ]
[ u_{ch,t} + u_{dis,t} \le 1 ]
[ SOC_{t+1} = SOC_t + \eta_{ch} P_{ch,t} \Delta t - \frac{P_{dis,t} \Delta t}{\eta_{dis}} ]
最后这条SOC递推式是整个储能模块的核心——如果这里写错,后面的调度结果会乱成一团。尤其要注意两个细节:一是将最大充放电功率与能量容量的比值对应上;二是充电效率和放电效率不要混用,前一个乘在充电功率上,后一个除在放电功率上,反了就相当于储能又发电又耗电,得不偿失。
2.3 第二阶段可调变量如何在约束中体现
多阶段鲁棒模型的独特之处在于变量的层级。用x表示第一阶段的整数变量(机组启停、储能充放状态、购售电状态),用y表示第二阶段连续变量(各电源出力、储能功率)。当不确定参数z实现后,系统可以选择不同的y值来应对。
数学上,约束写成:
[ \forall z \in Z,\quad \exists y :\quad A x + B y \ge C(z) ]
这个“存在量词”非常关键。它表示系统具备“看到z之后再调整y”的能力,这正是多阶段鲁棒和单阶段鲁棒的本质区别。约束设计得对不对,直接决定模型的保守程度:可调变量自由度越大,系统越灵活,成本越低。
2.4 为什么说储能是鲁棒调度的“天然缓冲器”
从物理上看,储能是微电网中唯一能在分钟级时间尺度内双向调节功率的设备。光伏出力骤降时,储能可以立刻从充电模式切换到放电模式;负荷突然拉高时,储能也能第一时间响应。所以在鲁棒调度模型中,储能SOC的运行区间和充放电功率上限,决定了系统“吸收不确定性的能力上限”。
在CCG迭代过程中可能会观察到有意思的现象:裕量越大的储能配置,让模型找到的“最恶劣场景”往往也更极端——因为系统有余量去应对更极端的工况,而不至于让问题不可行。这是配置储能容量时最值得关注的一组对比实验。
3. CCG求解框架:主问题—子问题迭代的完整流程
3.1 为什么min-max-min结构的模型不能直接丢给求解器
“对任意不确定参数都存在可行解”这个量化表达,让整个模型在数学上变成了一个三层优化问题,外层是最小化成本,中层是最坏场景搜索,内层是实时调节策略。这种结构不能直接用YALMIP加一个求解器选项解决,必须设计分解算法,把三层问题拆成主问题(MP)和子问题(SP)反复迭代。
列约束生成法(CCG)是目前求解两阶段鲁棒优化最主流的方法之一,思路很直接:先猜一个不确定场景,在这个场景下求解主问题得到一组决策,然后把“最坏场景搜索”的职责交给子问题——子问题找到当前决策下最不利的场景,给主问题“提意见”,主问题拿到意见后把新场景纳入约束再求解,如此循环,直到上下界收敛。
3.2 CCG迭代逻辑:上界和下界怎么更新
算法流程用伪代码表示如下:
初始化:场景集合 S = {预测场景 z0} LB = -inf,UB = inf for k = 1,2,3,... 求解主问题 MP(S),得到 (x_k, y_k) 和最优目标值 obj_MP LB = max(LB, obj_MP) 固定 x_k,求解子问题 SP(x_k),得到最恶劣场景 z_k 和最优目标值 obj_SP UB = min(UB, obj_SP) if UB - LB < tol: break else: S = S ∪ {z_k} end这里需要注意,不同文献对UB和LB的更新方式略有差异,因为有些模型的SP会给出总成本,有些只给出二阶成本或违反量。具体实现时,推荐的做法是同时记录每个阶段的原始目标值和惩罚项,自己先手推一遍第一个迭代轮次的数据,确认逻辑无误,再上代码。
3.3 主问题MP在MATLAB+YALMIP中的实现方式
MP的任务是在当前已知的有限个场景集合S中寻找最优的第一阶段决策,同时要求每个场景都满足约束。代码框架如下:
% 主问题建模 X_gen = binvar(n_gen, T); % 机组启停 X_start = binvar(n_gen, T); % 启动指示 P_dg = sdpvar(n_gen, T, 'full'); % 各机组出力 SOC = sdpvar(1, T+1, 'full'); P_ch = sdpvar(1, T, 'full'); P_dis = sdpvar(1, T, 'full'); P_buy = sdpvar(1, T, 'full'); P_sell = sdpvar(1, T, 'full'); Constraints = []; % 第一阶段公共约束,如机组最小启停时间、SOC边界等 ... for i = 1:length(S) % 对每个已知场景 z = S{i}; % 注意:每个场景独立的第二阶段变量 eval(['P_dg_scene', num2str(i), ' = sdpvar(n_gen, T, ''full'');']); % 加入功率平衡、储能约束等 Constraints = [Constraints, power_balance(z, ...)]; end Objective = sum(sum(f_dg(P_dg))) + ...; optimize(Constraints, Objective, sdpsettings('solver','gurobi','mipgap',1e-4));实际操作中更推荐用元胞数组存放不同场景的二阶段变量,避免使用eval动态建变量名——调试便利性会好很多。此前在项目中就是因为eval建变量导致迭代后期内存增长异常,换成元胞数组后才彻底解决。
3.4 子问题SP的对偶变换与线性化要点
SP的目标是在给定第一阶段决策x后,在不确实定集合Z中找到一个场景,使系统的“违规成本或阶段二总成本”最大化。形式是一个max-min问题,内层的min是一个线性规划。为了求解,需要对内层LP取对偶,把min-max转成单层max问题。
对偶变换后会出现两类麻烦:
一是内层变量的对偶变量与不确定变量z相乘产生的双线性项。处理办法是引入Big-M系数,用MILP将其线性化。二是互补松弛条件是非线性的,也需要用Big-M配合辅助0-1变量处理。
我给出一个经过简化的SP建模要点:
% 子问题建模(给定 x_fixed 后) z_pv = sdpvar(1,T,'full'); % 光伏偏差变量 z_load = sdpvar(1,T,'full'); % 负荷偏差变量 % 不确定集合约束:|z|<=1,预算和 <= Γ Constraints = [ -1<=z_pv<=1, -1<=z_load<=1, sum(abs(z_pv))+sum(abs(z_load)) <= Gamma ]; % 内层LP的对偶变量 lambda = sdpvar(1,T,'full'); % 将内层min问题的对偶约束加入 Constraints = [Constraints, dual_feasibility(lambda, ...)]; % 目标函数:线性化后的最坏场景成本 Objective = -sum(lambda .* (P_pv_ref + delta_pv .* z_pv ...)); optimize(Constraints, -Objective, sdpsettings('solver','gurobi'));这里需要注意绝对值约束的写法。YALMIP支持直接写abs(z),求解器转换时会引入辅助变量;如果自己手动线性化,定义两个非负变量表示正负偏差再求和,效率更高。
3.5 收敛速度与求解器配置实测参考
在T=24时段、单台柴油机、一台储能、光伏+风电+负荷三类不确定源的复现案例中,Γ=2时模型通常经过5到8次迭代即可收敛,单次MP求解约0.5到2秒,SP约0.3到1秒,总耗时大约10到25秒。这个效率对研究和教学已经足够友好。
如果迭代出现震荡不收敛,建议先检查SP返回的最恶劣场景是否真的为“新场景”——如果每一轮都返回同一个场景,说明MP中对应的约束可能没有正确加入,或者SP的目标函数方向写反了。
4. 复现中的四大坑与调试记录
4.1 MP出现不可行解:某些极端场景下系统确实转不过来
第一次跑通迭代之前,我在MP环节卡了很久,现象是主问题报“infeasible”,调度方案在两个极端场景之间反复摇摆,最终无法收敛。后来复盘发现原因很直接:当SP把某个最恶劣场景返回给MP后,MP要求系统在“光伏极低、负荷极高、同时储能SOC已接近边界”的条件下满足功率平衡,但柴油机的最小技术出力、储能最大放电功率加在一起就是不够用。
解决办法是在模型中增加一个“虚拟调节变量”表示允许切负荷或弃光,并在目标函数中设置很高的惩罚系数。这么做有两个好处:一是数学上保证MP始终可行,CCG迭代不会因为infeasible而崩溃;二是SP可以在更真实的物理场景下搜索“最恶劣程度”,而不是把所有问题都归结为不可行。实际工程中,极端场景下适当切负荷本身也是调度员的最后手段,模型有惩罚项反而更贴近真实运行逻辑。
4.2 SOC递推公式的单位“隐形陷阱”
这个坑特别隐蔽,对新手尤其致命。储能的功率单位通常是kW,容量单位是kWh,充放电1小时功率值等于能量值,所以递推公式里乘1小时看起来毫无问题。但如果某个时段的间隔不是1小时,或者代码里用了分钟作为时间单位,SOC递推就会偏离预期,结果表现为SOC曲线莫名其妙地“越界”或剧烈跳跃。
更隐蔽的情况是储能最大充电功率100kW,容量500kWh,表示1小时内最多能充入容量的20%,并非想充多少就充多少。如果只约束SOC范围却不考虑功率约束与容量的匹配关系,优化结果会在个别时段出现“SOC从20%跳到90%”这种物理上实现不了的计划。我建议一开始就把所有功率统一为kW、能量统一为kWh、时间统一为1小时,在注释里写清楚单位关系,能省下大量调试时间。
4.3 SP对偶变换中双线性项处理不当导致非线性
这个问题尤其容易出现。将内层min问题对偶化之后,目标函数或约束中会出现对偶变量λ与不确定变量z的乘积项λ·z。如果不做处理直接丢给求解器,YALMIP会报模型是非线性的,或者Gurobi直接拒绝求解。
Big-M线性化是标准解法。比如目标函数里有 ( z \cdot \lambda ) 项,可定义一个辅助变量 ( w = z \cdot \lambda ),加入约束:
[ w \le M z,\quad w \ge -M z,\quad w \le \lambda + M(1-z) ]
这里的M需要取足够大但又不能太大,否则会带来数值稳定性问题。实际操作时,我通常会根据负荷量级设置M,比如系统最大功率200kW,取M=500就够用,而不会拍脑袋设成1e6。
4.4 求解器数值不稳定与结果抖动
数学上完全正确的模型,落到具体数值求解时也可能出现抖动。我在项目里遇到的现象是:前两轮迭代UB稳定下降,第三轮却突然跳高,之后又回落,曲线像心跳图一样。
最终定位到两个原因:一是模型中成本系数跨度太大,启动成本500元、购电成本0.5元/kWh,相差1000倍,导致求解器数值病态;二是某个Big-M参数设得过大。处理办法是把成本系数归一化,购电和启动成本都折算到同一个数量级;同时在求解器设置中指定MIPGap为1e-4或精确求解,而不是默认的1e-4有时会自动放宽。
下面这张表是我整理的最容易出问题的点、现象和处理办法,可以直接当作调试手册用。
| 问题类型 | 现象 | 常见原因 | 处理方案 |
|---|---|---|---|
| MP不可行 | 主问题报infeasible | 极端场景下调节能力不足 | 引入切负荷/弃光虚拟变量加高惩罚 |
| SOC越界 | 储能SOC曲线跳跃或超限 | 时间单位或功率能量换算错误 | 统一单位并检查递推公式系数 |
| 双线性项未处理 | 子问题报非线性 | 对偶变量与z相乘 | 用Big-M线性化辅助变量 |
| 数值震荡 | 迭代结果上下跳动 | 系数跨度过大,Big-M过大 | 归一化单位,固定MIPGap,缩小M |
5. 结果验证:从图上就能看出鲁棒性的价值
5.1 一个可复现的算例参数与运行结果
复现案例采用一个典型区域微电网结构:光伏装机100kW,风电装机50kW,柴油发电机200kW,储能容量80kWh、最大充放电功率20kW,允许与主网购售电,24时段为单位。取Γ=3进行鲁棒优化。
完成迭代后在我的复现中得到的典型结果如下。
| 指标 | 确定性优化(Γ=0) | 鲁棒优化(Γ=3) |
|---|---|---|
| 总运行成本 | 1650元 | 1930元 |
| 最恶劣场景下切负荷量 | 15kWh | 0kWh |
| 柴油机启停次数 | 3次 | 5次 |
| 储能平均SOC | 52% | 61% |
结论非常直观:鲁棒优化用17%左右的成本涨幅,换来了“最恶劣场景下零切负荷”。对需要保障供电可靠性的区域微电网来说,这笔溢价通常是可以接受的。
5.2 最恶劣场景的特征:为什么总是“傍晚”
观察SP返回的最恶劣场景后,我发现高概率的极端场景总是集中在傍晚17:00到20:00。原因不难理解:光伏出力在这段时间持续下降直至归零,负荷却处于晚高峰,若此时预测误差也同时达到不利方向,系统就需要柴油机和储能同时承担大量调节任务。
这个发现的工程意义在于:与其在全天均匀预留备用,不如把备用能力重点集中在傍晚高峰时段。储能调度策略也会随之改变——在午后光伏大发时段提前把储能充满,到傍晚留足放电空间,而不是盲目地在每个时段都维持高SOC。
5.3 储能SOC曲线的“鲁棒特征”
对比确定性方案和鲁棒方案的SOC曲线,能看到明显差别。确定性方案倾向把SOC压在较低区间,因为低SOC意味着充电空间大,能更多吸收光伏高峰电量,经济性更好。鲁棒方案则更“怕”出现光伏骤降的同时负荷爬升,所以SOC整体更高,留出更多可放电能量。
如果在图上把SP找到的最恶劣场景画成阴影区,再叠加调度方案,能很直观地看到每一段决策是否覆盖了偏差范围。这个方法强烈推荐——它比任何数字指标都更能帮你判断模型行为是否合理。
6. 从“能跑通”到“有研究价值”:参数敏感性分析与后续扩展
6.1 预算参数Γ对成本和迭代次数的影响
我刻意对不同Γ值做了敏感性测试,捕捉到一种典型规律:Γ从0增加到2时,总成本涨幅明显;从2再增加到4、5时,成本增速放缓,最终趋于饱和。原因在于当预算增大到一定程度后,SP能搜到的“真实最恶劣场景”基本到头了,继续加预算只是让更多时段保持极端偏差,但系统调节能力已经到顶,边际成本自然递减。
| Γ | 总成本 | CCG迭代次数 |
|---|---|---|
| 0 | 1650 | 1 |
| 1 | 1762 | 3 |
| 2 | 1855 | 5 |
| 3 | 1930 | 7 |
| 4 | 1978 | 8 |
| 5 | 2011 | 9 |
这个表非常直观,用来写报告、做答辩材料都够用。我自己的体会是,论文里报告一个适中的Γ值完整方案,再把敏感性分析作为补充,是最常见的呈现方式。
6.2 从两阶段鲁棒走向分布鲁棒
经典两阶段鲁棒模型假设不确定集合是确定的,但现实中我们往往只知道预测误差的历史样本,不清楚确切的概率分布。分布鲁棒优化在这个基础上引入一个“模糊集合”,要求概率分布落在以经验分布为中心的某个Wasserstein或矩约束球内,优化目标变成最小化最坏分布下的期望成本。
这个方向已经是当前调度领域的研究热点,而且与本文的基础模型天然衔接:只需要把不确定集合约束替换为分布模糊集合,内层问题复杂度上升,但外层CCG框架仍然适用。如果你想在这个模型基础上做创新,优先考虑这个方向。
6.3 多微网互联与共享储能的模型扩展
区域微电网的边界还可以向外扩展。多个微网之间用联络线相连,功率可以在微网之间双向流动,储能也不必归属于某个单一微网,而是配置在公共节点共享。模型的决策变量会增加联络线功率和共享储能SOC两个维度,功率平衡从“单节点等式”变成“多维网络方程”,但整体仍在本文的CCG框架内可解。
这类扩展特别适合毕业论文或科研项目:基础模型做扎实,扩展方向做增量贡献,比重新搭建一套新模型要务实得多。
最后再分享一个实际体会:复现这套模型时,我踩过最多坑的地方反而不是数学公式,而是MATLAB中变量管理和代码结构规范——YALMIP变量在迭代中不断累积,约束数组越来越长,后面调试几乎寸步难行。建议从一开始就把每个独立功能写成函数,用元胞数组管理多场景二阶段变量,把“一个模块一个脚本,一类变量一个函数”的原则执行到位,跑通和扩展只是时间问题。