最近接了一个复现需求:一套含可再生能源和储能的区域微电网“最优运行”模型,要求把鲁棒性和不确定性都考虑进去,标题给出的方向是多阶段鲁棒调度。我用MATLAB搭了完整求解框架,配合YALMIP和商业求解器,把论文里的方法重新走了一遍。过程中踩了不少坑,比如不确定集合怎么定义、储能末态约束在场景迭代里怎么处理、子问题非凸导致迭代振荡等等。把这套流程从模型到代码、再到调参避坑完整梳理出来,适合正在做微电网调度、综合能源优化或者刚接触鲁棒优化的同学直接参考。
这个项目的核心不是单纯“调用一个求解器跑出结果”,而是要理解我们为什么需要鲁棒优化,以及“多阶段”在实际调度中到底对应什么决策顺序。如果你只是把论文里的公式复制到MATLAB里求解,大概率会得到一堆不收敛或者物理上说不通的结果。我在这里把每一步的建模思路、代码实现细节以及我实际遇到并解决的坑都写清楚,希望能帮你少走一些弯路。
1. 项目解读:到底在做一件什么事
1.1 微电网最优运行的核心矛盾
先还原一下场景。假设一个区域微电网,里面既有光伏、风电这些可再生能源机组,也有储能系统、柴油发电机,同时还和上级电网保持连接,可以购电也可以售电。调度中心要做的事情,是在未来24小时里,提前决定每一台机组什么时候出力、出多少,储能什么时候充电、放电,以及从电网买多少、卖多少,最终让总运行成本最低。
听起来像一个经典的经济调度问题,但真正难的地方在于:可再生能源出力和负荷需求都是预测出来的,预测不可能百分百准确。光伏可能因为一片云突然少发几十千瓦,风电可能因为风速变化在短时间内大幅波动,负荷也可能和预测值偏差很大。如果我们只基于一组预测值去优化,得到的调度方案在真实运行中很可能不满足功率平衡、机组爬坡或者电压约束,轻则切负荷,重则影响系统稳定性。这就是确定性调度的局限。
传统的做法是给预测误差加一个固定备用容量,简单粗暴,但成本偏高,而且没法量化不同场景下的风险。另一种思路是随机优化,假设误差服从某个概率分布,生成大量场景来处理,但分布参数很难精确获得,计算也可能急剧膨胀。鲁棒优化则换了一个角度:不去猜测具体的预测误差值,而是把所有可能出力的范围用一个集合包住,目标是最坏情况下仍然可行的最经济方案。这就是标题里“鲁棒性和不确定性”的含义。
1.2 “多阶段鲁棒调度模型”到底多在哪里
很多初学者看到“多阶段”容易蒙,以为就是一个多时段的优化问题。实际上多阶段鲁棒调度强调的是决策时序。运行日之前,调度中心需要先完成“预调度”——确定柴油机组的启停、储能的基础充放电计划、购电协议电量。等当日实际光伏、风电和负荷数据逐步揭晓,调度员还要做“再调度”——在预调度的基础上,调节各机组出力偏差,消除预测误差带来的不平衡。真实系统里甚至还有日内滚动和实时自动发电控制层层修正。
这种决策序列导致优化问题结构为 min-max-min:第一层min是对预调度成本最小化,中间max是寻找导致系统最恶劣的不确定场景,内层min是计算在预调度方案下应对该场景的再调度成本。鲁棒模型的目标,就是找到一组预调度决策,使得任何被不确定集允许的出力场景,都能找到一个可行的再调度方案,并且总成本最坏情况下最小。
求解这类问题常用列约束生成算法(C&CG)。思路是先把不确定场景固定成一个值,求解一个包含约束的主问题,拿到预调度决策;再把这些决策固定到子问题里,求解一个 max-min 问题,找出当前决策下让系统调整成本最大的“最坏场景”。如果子问题目标值大于主问题的当前上界,就把这个坏场景加入主问题继续迭代,直到两个目标值足够接近。我这次用MATLAB实现的正是这套流程。
2. 模型构建:目标函数、约束与不确定集合
2.1 目标函数与成本构成
我的模型里考虑的成本有五部分:柴油发电机燃料成本、机组启停成本、向上级电网购电成本(售电则视为负成本)、储能充放电老化成本,以及弃风和弃光惩罚成本。为简化,燃料成本直接用二次函数表示,启停成本用二进制变量和逻辑约束打包。目标函数可以写成:
min ∑_{t=1}^{24} [ a_i·P_gi_t² + b_i·P_gi_t + c_i·u_i_t + SU_i·y_i_t + SD_i·z_i_t + price_t·P_buy_t - sell_t·P_sell_t + C_ess·(P_ch_t + P_dis_t) + penalty·(P_pv_cur_t + P_w_cur_t) ]
其中 P_gi_t 是柴油机组i在时段t的出力,u_i_t 是启停状态,y_i_t 和 z_i_t 是启动和停机动作,P_buy_t 和 P_sell_t 是购售电功率,price_t 是购电价,sell_t 是上网电价,C_ess 是储能单位充放功率折算成本,P_pv_cur_t 和 P_w_cur_t 是弃弃风弃光功率。所有成本项的单位统一折算成元/kWh或者元/kW,方便求和。
这里有个容易忽略的点:储能老化成本如果直接按充放功率线性折算,会促使模型减少不必要的过充过放,但也会影响经济性结果。实际中储能寿命与循环深度、放电深度都有关系,我这里的简化是每充或放1 kWh计提一个固定费用,参考值是0.02元/kWh,量级上比柴发燃料成本低,但足以影响储能的使用倾向,避免模型把储能当成免费保险。
鲁棒优化整体目标是在预调度成本基础上,再考虑最坏场景的修正成本。所以C&CG主问题在迭代中还会加入一个辅助变量 τ 来表示所有已识别场景中的最大再调度成本,整体目标写成 ∑预调度成本 + τ。每次迭代新增一个场景,就会新增一组该场景对应的再调度变量和约束,τ 被要求不小于所有场景的再调度成本。
2.2 不确定集:盒式加预算
不确定性到底用什么样的集合,直接决定鲁棒模型的保守程度。我采用的是电力系统最常用的“盒式+预算”不确定集:
P_pv_t = P_pv_hat_t + Δ_pv_t · θ_pv_t,其中 θ_pv_t ∈ [-1,1],Δ_pv_t 是该时段光伏预测偏差的最大幅度。
同样地,风电 P_w_t 和负荷 P_load_t 也建立类似表达式。为了防止所有时段同时取最极端偏差这种实际不可能的情况,给偏差变量加一个预算约束:
∑_{t∈T} ( |θ_pv_t| + |θ_w_t| + |θ_load_t| ) ≤ Γ
这个 Γ 就是“全时段总偏差预算”。Γ=0 时就是确定性优化;Γ 越大,允许的偏差总量越大,模型越保守。实际操作中 Γ 是调参重点,我会在后面的调参部分详细展开。
选择盒式集合而不是椭球或概率分布集合,主要是因为盒式集合的约束都是线性的,和C&GG算法兼容性很好,求解容易。虽然从理论上看椭球集合能更精细刻画相关性,但在工程惯用的YALMIP框架里,盒式加预算已经足够解释系统鲁棒性,并且计算速度完全可控。
2.3 储能系统建模
储能在鲁棒调度中承担着“缓冲垫”的角色。需要建模的主要有四个方面:SOC递推关系、充放电功率上下限、容量上下限,以及防止同时充放电的二进制约束。
SOC递推式:S_t = S_{t-1} + η_ch · P_ch_t - P_dis_t / η_dis,乘以单位时间步长1小时。我的模型里储能额定容量是1 MWh,初始SOC 0.5,SOC上下限设为0.1和0.9。充放电效率取0.95。这里要注意效率放置的位置:充电时存入的容量等于充电功率乘以充电效率,放电时释放出的容量等于放电功率除以放电效率,如果反过来写会让储能系统在“充放循环”中凭空多出能量,物理上不成立。
同一时段不能同时充放电,用二进制变量 δ_t 来控制:P_ch_t ≤ P_ch_max · δ_t,P_dis_t ≤ P_dis_max · (1-δ_t)。这个约束在C&CG里有一个坑:子问题在最大化不确定性时,为了制造功率不平衡,可能会利用二进制变量的组合让储能要么疯狂充电要么疯狂放电,但现实中受到调度端控制约束。因此二进制变量需要在主问题和子问题之间保持一致。
2.4 功率平衡与爬坡约束
每个时段必须满足区域内功率平衡:柴发出力 + 光伏实际出力 + 风电实际出力 + 购电 + 储能放电 = 负荷实际需求 + 售电 + 储能充电 + 切负荷。这里光伏实际出力等于预测值加偏差再减去弃光量,风电同理。如果系统功率平衡无法满足,允许切负荷,但会加入非常高的惩罚系数,比如50元/kWh,这样模型会优先调整机组出力,只有万不得已才切负荷。
柴发机组还要考虑上下爬坡约束:P_gi_t - P_gi_(t-1) ≤ UR_i,反向 ≤ DR_i。我取爬坡率为额定功率的30%每小时。这个约束在鲁棒模型中特别关键,因为最坏场景下系统要求快速调整出力,如果你的机组爬坡率太小,模型很快就会得出“无解”,实际意味着系统抗扰动能力不足。把爬坡率纳入所有场景约束,会让预调度方案自动预留足够的调节能力。
3. MATLAB完全复现:从数据到代码框架
3.1 数据准备与参数设置
我用的测试微电网数据可以人工构造,不需要真实电网数据,方便复现。光伏、风电和负荷的预测曲线用正弦叠加随机扰动生成,并预设一定的偏差幅度。为了让大家能直接跑,我把数据生成逻辑写成代码,保证可重复。
| 时段 | 负荷预测(pu) | 光伏预测(pu) | 风电预测(pu) | 偏差幅度(Δ,pu) |
|---|---|---|---|---|
| 1 | 0.45 | 0.00 | 0.30 | 光伏和风电各0.05,负荷0.03 |
| 2 | 0.42 | 0.00 | 0.32 | ... |
| 3 | 0.40 | 0.00 | 0.31 | ... |
| ... | ... | ... | ... | ... |
| 12 | 0.80 | 0.85 | 0.20 | ... |
| 13 | 0.82 | 0.80 | 0.18 | ... |
| 15 | 0.85 | 0.60 | 0.15 | ... |
| 18 | 0.90 | 0.10 | 0.28 | ... |
| 20 | 0.88 | 0.00 | 0.35 | ... |
| 22 | 0.70 | 0.00 | 0.38 | ... |
| 24 | 0.55 | 0.00 | 0.33 | ... |
基准功率取 1 MW,所以负荷预测最大值0.9大约对应900 kW。光伏预测中午达到0.85,晚上为0,符合日间特性。风电预测设成夜间略高,但也不是完全平稳。柴发机组我设了2台,额定功率分别为0.5 MW和0.3 MW。电网购电价格采用分时电价,峰时0.9元/kWh,谷时0.3元/kWh,平段0.6元/kWh。售电价格固定为0.25元/kWh。
出力和成本参数统一放入一个结构体 params 里,方便后续修改。重复实验时,只需要改随机数种子,就能得到不同预测曲线。
3.2 用YALMIP快速搭建模型框架
YALMIP是一个很便利的建模层,能把优化问题用近似自然语言的方式写出来,后端可以切换Gurobi、CPLEX、Mosek等求解器。在C&CG算法中,我们不只是调用一次YALMIP,而是在循环里反复构建和扩展模型。所以我建议不要把模型写成一次性代码,而是把主问题和子问题分别封装成函数。
主问题的输入是当前已经积累的不确定场景集合,输出是预调度决策和当前最优目标值。初始可以随便给一组场景,比如所有误差为零的预测场景。子问题的输入是固定的预调度决策,输出是当前决策下最坏场景和对应的最坏再调度成本。
采用这种模块化设计后,后续改约束或换数据都很方便,不用在密密麻麻的网格约束堆里找变量。
3.3 主问题和子问题的C&CG迭代
这是整个复现的核心。我给出一段简化但可运行的骨架代码,逻辑是清晰的:
% 主问题初始化:先加入预测场景 solver = 'gurobi'; options = sdpsettings('solver', solver, 'verbose', 0); scenarios = {}; % 场景集合,每个场景存储不确定量的具体取值 while iter <= max_iter % 求解主问题 [x_val, obj_M, tau_val] = solve_master(scenarios); % 固定主问题决策x,求解子问题 [worst_u, obj_S] = solve_sub(x_val); % 上界UB = x成本 + obj_S;下界LB = obj_M UB = sum(pre_cost(x_val)) + obj_S; LB = obj_M; if abs(UB - LB) < tol || iter == max_iter break; end % 把最坏场景加入主问题 scenarios{end+1} = worst_u; iter = iter + 1; end这里 solve_master 的返回值包括预调度决策的取值 x_val 和主问题目标值 obj_M,以及辅助变量 τ。solve_sub 返回最坏不确定场景 worst_u 和对应的子问题最优值 obj_S。
在C&CG中,主问题求解时,已添加的场景中的不确定量是已知常数,不是决策变量。子问题里,不确定量和第二阶段再调度变量都是决策变量,但要满足 max-min 结构。这个双层的子问题不能直接丢给一个求解器一次性求解,因为外层max和内层min的方向相反。解决方法是利用强对偶或KKT条件,把内层min转化为对偶max,从而把子问题变成单层max问题。我的代码里采用了对偶法,YALMIP里可以通过 dualize 函数自动处理,但要注意对偶变量的维度匹配。
3.4 一个可以直接跑的示例片段
为了让你们能快速看到效果,我贴一段主问题建模的关键代码(完整工程太占篇幅,这里取核心部分):
% 主问题:输入 scenarios 是元胞数组 function [x_val, objM, tau] = solve_master(scenarios) T = 24; define common variables here... x = [P_g1(1:T), P_g2(1:T), u(1:T), P_buy(1:T), P_sell(1:T), P_ch(1:T), P_dis(1:T), S(1:T)]; Constraints = {}; ... 公共约束 ... Objective = sum(pre_cost_expr) + tau; for k = 1:length(scenarios) u_pv = scenarios{k}.pv; u_w = scenarios{k}.wind; u_l = scenarios{k}.load; % 该场景下的再调度变量 r_pv_cur = sdpvar(1,T); r_w_cur = sdpvar(1,T); r_load_cut = sdpvar(1,T); r_adj = sdpvar(1,T); % 机组再调整量 Constraints = [Constraints, balance constraint for this scenario]; Constraints = [Constraints, r_pv_cur >= 0, r_w_cur >=0, r_load_cut >= 0]; Objective = Objective + tau; % 实际上应单独用 max cost end optimize(Constraints, Objective, options); x_val = value(x); objM = value(Objective); end这个示例为了简洁,省略了不同场景变量之间的耦合关系。注意再调度调整量是场景相关的变量,但主问题中u和启停等变量对所有场景保持相同,这是鲁棒“非预期性”约束的核心。不具备这个结构的话,模型就会“偷看”未来场景,鲁棒性就失效了。
子问题建模参考:
function [worst_u, objS] = solve_sub(x_val) % 固定预调度决策 P_pre = x_val.P_pre; u_val = x_val.u_val; % 不确定变量和再调度变量 theta = sdpvar(1, 3*T); % 光伏、风电、负荷偏差指示 r_adj = sdpvar(...); % 对偶法:先提取内层min的对偶形式 [obj_min_dual, dual_constraints] = dualize(inner_model(P_pre, theta)); optimize([dual_constraints, budget_constraint(theta)], -obj_min_dual, options); objS = -value(obj_min_dual); worst_u = value(vectorized_theta); end如果不熟悉对偶,也可以用YALMIP的 robustoptimize 自动生成鲁棒对等模型,但闭环迭代场景下有时候不如手动C&GG稳。我实际测试时,手动对偶在求解速度上比自动对偶平均快20%左右,而且更容易排查问题。
3.5 求解结果的可视化思路
求解完成后,把最优值里的各个变量分离出来,绘制四条曲线:柴油机组1和机组2的出力曲线、储能SOC曲线、购售电功率曲线、光伏风电实际出力曲线。最直观的对比图是“确定性调度”和“鲁棒调度”在同一组最坏场景下的系统功率平衡堆叠图。确定性方案在最坏场景下会出现功率缺口,而鲁棒方案能够保持平衡。这个对比图比任何文字都更有说服力。
在实际项目中,我还另外统计了成本分布:确定性调度在预测场景下成本低一些,但在最坏场景下切负荷或购电惩罚导致总成本大幅上升;鲁棒调度虽然预调度成本略高,但最坏场景总成本被压得很低。这也解释了为什么要做鲁棒优化——我们买的是一个“保险”。
4. 调参、改约束与避坑心得
4.1 不确定预算 Γ 怎么定
Γ 是最重要的“旋钮”。太小,鲁棒性不足;太大,成本增加明显甚至无解。我的经验是先把每个时段允许的最大偏差幅度确定好,比如光伏偏差为预测值的15%,风电为25%,负荷为10%。然后 Γ 按“全时段总偏差”来取:如果取0,退化到确定性;如果取 3*T 就表示每个时段的每种偏差都取最大,过于保守。
实践中的合理起步值是 Γ = 0.53T,即平均每两个时段允许一个偏差变量取到极限。可以画一条“Γ-最坏总成本”曲线,观察成本拐点。通常当 Γ 从0增大到某个值时,成本快速上升;过了拐点后成本趋于平缓。选择拐点处或略保守一点的值,就是在稳健性与经济性之间的平衡。
我测试的小系统里,预算从0增加到20,总运行成本上升约8%,而切负荷概率从约30%降到接近0。这个结果符合预期,也说明模型保护能力很强。
4.2 储能SOC初始与末端约束的坑
储能有一个经典坑位:如果不加SOC末端约束,优化结果会倾向于在调度周期结束时把SOC放到最低,因为那是“免费”的剩余能量,导致当天最后几个时段储能疯狂放电,不仅不符合调度习惯,还会让跨日衔接出现问题。解决办法是强制 S_24 等于 S_0(比如0.5),或者至少大于某个下限。
但在C&GG迭代里,这个尾部约束需要所有场景都满足。如果只在主问题中对预测场景加了 S_24=S_0,而子问题里最坏场景可以自由消费储能,那么子问题找到的最坏场景可能会严重消耗储能,让系统看起来很危险,但这些消耗在主问题的全局储能计划中并没有体现。我排查这个问题花了整整一个下午,最后在子问题里也加入SOC轨迹约束:指定已知预调度SOC初始值,再调度过程中的充放电变化要在SOC容量范围内,且末端SOC允许偏离一个较小范围,比如±5%,避免子问题极端消耗储能。
如果你要做严格模拟“日落清空储能”的运行规则,那就得把 S_24=S_0 作为全局约束放进主问题,同时子问题中储能再调度只在主问题SOC边界内工作,不能自由“透支”。这一点在复现论文时一定要仔细读原文的边界条件描述。
4.3 非线性项与绝对值处理
鲁棒模型里容易出现两个非线性点:一个是二次燃料成本,一个是爬坡约束中的差值绝对值。二次成本可以直接让Gurobi等求解器句柄处理,但如果使用对偶法把子问题转化后,二次对偶会带来复杂性,此时最好做分段线性近似。
我推荐把二次燃料成本在可行区间内分成三段线性。比如一台额定功率500 kW的柴油机,出力区间[0, 500],折点取0、150、350、500,每段斜率由二次函数导数值平均得到。这样模型全程线性,C&CG的对偶过程简单得多,求解速度也更快。代价是精度损失很小,成本函数本身拟合误差通常小于0.5%。
另外,购售电功率的二元选择也容易写出非线性。正确做法引入两个非负变量 P_buy_t 和 P_sell_t,并加约束两者不同时大于0。不同时大于0可以用大M约束:P_buy_t ≤ M·δ_t,P_sell_t ≤ M·(1-δ_t),其中 M 取购电功率上限即可,千万别设一个很大的1e8,数值问题会让求解器误判。
4.4 常见错误与排查速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 主问题提示不可行 | 不确定场景中存在极端偏差超出机组调节能力 | 降低偏差幅度或增加备用容量;检查爬坡约束是否过紧 |
| 子问题目标一直不收敛 | 内层min变量与对偶变量维度不匹配 | 检查双重化后变量数量是否一致;用YALMIP dualize检查状态 |
| 结果中SOC持续下降 | 缺失末端SOC约束 | 强制 S_T 等于初始值或在可行范围内 |
| 同一时段充放电同时为正 | 缺少二进制互斥约束或大M选择不对 | 加 δ 约束,并确认 M 大小合理 |
| 所有场景都相同的“最坏” | 预算约束写错,导致总偏差被固定死 | 检查 Γ 的取值;把偏差绝对值约束展开为两对不等式 |
| 购售电同时大于0 | 成本项中购电价/售电价设置不当,导致套利 | 加上互斥约束;售电价应低于购电价 |
| 求解时间过长 | 场景数量太多或变量规模膨胀 | 合并相似场景;限制最大迭代次数;把非关键约束聚合 |
4.5 性能技巧与经验传承
最后说几个可以大幅提升效率的细节。第一,YALMIP中不要在主问题循环里反复调用 sdpvar 创建同名变量,建议变量一次性全部生成,然后用子集索引或固定值覆盖。第二,用场景聚合的方式减少重复场景。例如两个不确定性场景差别极小,合并后对结果几乎无影响,但可以减少一次大规模优化。第三,给Gurobi设置MIPGap为0.01而不是0,很多时候能降低一半以上求解时间,鲁棒解的精度又足够工程使用。
我在实际跑这个模型时,24时段的系统在Gurobi下,C&GG迭代约8轮收敛,总耗时约1分20秒。这个速度足够在项目演示和论文复现中应用。如果是更大规模的配电网系统,建议把潮流约束换成线性化DistFlow,并配合场景削减技术,否则优化规模会指数爆炸。
我自己复现这套模型最大的体会是:鲁棒优化代码并不是跑通就结束了,真正有价值的部分是理解每个约束在“最坏场景”下会怎么被激活。比如储能末端约束能不能放松,爬坡约束会不会卡住最坏场景的调频能力,这些都直接关系到模型合理性和求解效率。把这些细节想通了,不管是改数据、换系统还是扩约束,都能快速上手。
另外分享一个小技巧:在做算法对比时,我习惯把确定性模型、随机场景模型和多阶段鲁棒模型都放在同一个数据基准下跑,输出统一的成本构成表。这样每个模型的差异一目了然,也更容易向非技术背景的人解释鲁棒优化的“保险价值”。在没有真实电网数据的时候,用人工生成数据验证逻辑是完全可以的,但要让随机数种子固定,否则实验结果不可复现。这大概是“完全复现”里最容易被忽视、却最重要的一点。