news 2026/9/9 18:54:51

两阶段鲁棒优化微网经济调度:MATLAB+YALMIP+CPLEX实战与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
两阶段鲁棒优化微网经济调度:MATLAB+YALMIP+CPLEX实战与避坑指南

做微网调度有一段时间的朋友,大概率绕不开一个问题:风电和光伏出力不确定,负荷也在波动,怎么安排机组出力才能既经济又稳得住?这个问题看着简单,真正落地的时候坑很深。今天这篇就从一个完整的MATLAB实现讲起——基于两阶段鲁棒优化算法的微网经济调度,代码配合YALMIP和CPLEX,把建模、求解、调参、避坑全过一遍。如果你正在做电气、能源方向的课题,或者刚接触鲁棒优化、想找一个能跑起来的微网算例,这篇应该能帮你省不少查资料的时间。

这篇内容不打算只贴一份代码,我会把背后的优化逻辑拆开讲。为什么两阶段,为什么用鲁棒而不是随机规划,不确定集怎么设,C&CG算法怎么一步步迭代,以及我在实际调试中遇到的几个让人抓狂的问题。文章偏实操,但模型部分也不会跳过,因为不理解模型,代码改了只会越改越乱。

1. 项目整体设计与思路拆解

1.1 微网经济调度到底在优化什么

微网可以简单理解成一个“小号的电力系统”,里面有柴油机、燃气轮机这类可控机组,有光伏、风电这类不可控电源,有储能电池,也可能和外部大电网有联络线。经济调度的目标就是:在满足负荷需求的前提下,安排每一台机组发多少电、储能充还是放、要不要从电网买电,让总运行成本最低。

成本一般包括燃料成本、启停成本、向电网购电成本,还有弃风弃光惩罚、切负荷惩罚等。燃料成本最常用的是二次函数,优化模型里通常做线性化处理,用分段线性近似。这里面有一个容易忽略的点:调度不是只看某一个时刻,而是要把24小时甚至更长时间尺度放在一起看,储能就是典型的跨时段耦合元件,这一小时充的电可能下小时才放,所以约束要写成时序递推的形式。

如果所有参数都是确定的,这其实就是一个典型的混合整数线性规划,直接用CPLEX或Gurobi就能解,网上也有大量确定性微网调度代码。但现实情况是,光伏出力受天气影响,早上还是大晴天,下午一朵云飘过来出力就能掉一大截;负荷也有随机波动。要是完全按预测值调度,实际运行时可能出现两条路:要么切负荷,要么加大备用,代价都不小。

1.2 为什么一定要用两阶段鲁棒优化

面对不确定性,常见方案有三种。

第一种是确定性优化,把预测值当成真值,算出来只能参考,不具备抵御预测误差的能力。第二种是随机规划,给不确定参数假设一个概率分布,生成若干场景去求期望成本。随机规划的缺点是,分布假设很难准,而且当概率分布有偏差时,得到的最优解可能在真实场景下严重越限。第三种就是鲁棒优化,不依赖概率分布,只要求不确定参数落在一个集合内,优化结果是“在最坏情况下也满足约束”的调度方案。

两阶段鲁棒优化的“两阶段”对应到微网调度里,有非常直观的物理含义:第一阶段是日前决策,决定机组启停、是否需要预购电量、储能是否进入某种运行模式,这些决策在不确定量实现之前就要定下来;第二阶段是日内调整,在风光实际出力出来之后,微网可以在允许范围内调整机组出力、储能充放电功率,甚至切除部分负荷,来保证功率平衡。

这个结构很像日常生活中“先定计划,再按实际情况微调”。第一阶段决策不能太激进,因为要为最坏情况留出调整空间;第二阶段调整量又不能无限大,受机组出力上下限、爬坡速率、储能SOC等物理约束限制。两阶段鲁棒优化就是研究这两者之间的平衡,目标函数里同时包含第一阶段的固定成本和第二阶段在最坏场景下的运行成本。对于微网这种连续变量为主、整数变量可控的调度问题,两阶段鲁棒优化的求解精度和解的可解释性都不错,这也是它成为热点的主要原因。

1.3 技术选型:MATLAB + YALMIP + CPLEX

我见过不少课题组用Python+Gurobi做微网调度,但MATLAB在电气工程领域还是基础盘,尤其对刚接触优化的研究生来说,理解矩阵操作和线性代数更顺手。YALMIP是一个MATLAB下的建模语言,它把sdpvar、binvar、optimize这些函数封得很漂亮,写约束就像写数学公式一样。CPLEX则是一个高性能的商业求解器,适合解大规模混合整数线性规划,和MATLAB之间有官方接口,YALMIP也内建了CPLEX的调用入口。

这套组合有三个好处:第一,建模快,不需要自己实现单纯形法或分支定界;第二,调试直观,YALMIP可以打印约束数量、变量类型,也能直接导出模型,方便定位问题;第三,两阶段鲁棒优化的C&CG算法需要反复求解主问题和子问题,MATLAB脚本控制循环很方便,YALMIP再配合CPLEX的求解器参数,可以省掉大量对象化编程的额外负担。

当然也可以选Gurobi,求解速度在很多测试上比CPLEX还快一点。但CPLEX在学术界的普及度高,许可证申请也方便,而且YALMIP的兼容性很好,换求解器只需要改一行sdpsettings。下面的内容我都以CPLEX为例,如果你用的是Gurobi,代码逻辑完全不用改。

2. 两阶段鲁棒模型的核心细节

2.1 不确定集:管住预测偏差

鲁棒优化最难也最关键的一步,是不确定集的选取。不确定集就是描述“不确定参数可能取哪些值”的集合。选得太大,调度结果会非常保守,成本高到离谱;选得太小,又失去了抵抗风险的意义。最常用的有两种。

一是盒式不确定集。每个时段的预测误差独立,光伏出力允许在预测值上下浮动不超过给定的偏差上限。写成形式就是:

u_t ∈ [p_fc_t - ε_t, p_fc_t + ε_t]

这个模型的优点是简单,缺点是把所有时段的最坏情况同时考虑进去,有点“把所有坏运气都押在一起”,太保守了。

二是预算不确定集。它在盒式基础上加了一个限制,比如所有时段误差归一化后的绝对值之和不超过某个预算Γ。这个Γ一般叫“鲁棒预算”,含义是:真实场景不可能所有时段都出现最大偏差,大部分情况下只有少数时段误差较大。预算不确定集能显著降低保守性,而且Γ可以从0取到T,Γ=0时退化为确定性模型,Γ=T时又回到盒式模型。

实际建模时,我建议先用盒式不确定集把流程跑通,再改成预算不确定集做对比分析。因为预算不确定集在子问题对偶后会出现双线性项,虽然可以通过线性化技巧处理,但对新手不太友好。我们后面写C&CG时,子问题会用对偶把min问题转成max问题,这时如果U是盒式集合,最坏场景可以直接根据对偶变量的符号确定,省去很多麻烦。

2.2 两阶段决策变量与目标函数拆分

两阶段模型里,变量要严格区分开。

第一阶段变量通常包含0/1变量,比如机组开机/停机状态、储能是否充电/放电状态;也包含一部分连续变量,比如日前预调度出力。这些变量需要在不确定量实现前确定,所以在CCG主问题中它们是决策变量。

第二阶段变量是“看到不确定量之后才能做出决定”的连续变量,比如实际机组出力调整量、储能实际充放电功率、切负荷量、购售电功率。它们的可行域会依赖第一阶段变量和不确定参数。

目标函数也要拆开。用紧凑形式可以写成:

min_x a^T x + max_u∈U min_y∈Y(x,u) d^T y

外面的min处理第一阶段变量x,里面的max-min就是在找最坏不确定场景下的最小调整成本。这里a^T x对应机组启停成本、固定运行成本;d^T y对应燃料成本、购电成本、切负荷惩罚、弃风惩罚等。

为什么不是直接min总成本?因为不确定参数u还没有实现,第二阶段的收益/成本必须以最坏情况来评估。这个结构是两阶段鲁棒优化的标准范式,也是后面C&CG每个迭代步的数学基础。

2.3 约束怎么列才不乱

微网调度约束看着多,其实可以分成三类。

第一类是第一阶段约束,只含x和辅助变量,比如机组的最小启停时间约束、储能是否允许同时充放电的状态约束。注意,这类约束不含不确定量,所以不会因为场景变化而改变。

第二类是第二阶段约束,包含x和y,且被不确定参数u影响。典型的是功率平衡约束:

∑ P_g_t + P_pv_t + P_es_dis_t + P_buy_t = L_t + P_es_ch_t + P_sell_t + P_curtail_t

这里的P_pv_t就是一个不确定参数,同时在第二阶段的调整中存在弃光变量P_curtail_t,允许在最坏情况下切掉一部分光伏,代价是惩罚项进目标函数。

第三类是机组运行约束,比如出力上下限、爬坡约束、储能SOC递推约束。这些约束虽然不含u,但由于第一阶段x固定后,第二阶段y的可调范围被x限制,它们直接决定了最坏场景下的调整能力。

很多初学者喜欢把全部约束一股脑写进一个约束体,然后求解。在确定性模型里没问题,但在两阶段鲁棒优化里,如果主问题和子问题共用同一个YALMIP约束体,会导致变量嵌套、迭代混乱。我的经验是:把第一阶段约束、第二阶段约束写成独立的函数或脚本段,每个阶段单独构建变量和约束,用函数参数传入已知变量值,这样C&CG循环才清晰。

3. 实操过程:从环境配置到CCG求解

3.1 环境准备:YALMIP和CPLEX装到位

先说你需要的软件:MATLAB(R2020a及以上版本),YALMIP(最新版),CPLEX(12.10或更高版本,需要去IBM官网申请学术许可)。装好的标准是,在MATLAB命令行输入yalmiptest,能看到CPLEX状态为正确:

addpath(genpath('D:\yalmip')); savepath; yalmiptest

如果yalmiptest结果里CPLEX显示失败,多半是CPLEX的MATLAB接口路径没配好。CPLEX自带的接口文件在cplex\matlab\目录下,也需要加入MATLAB路径。我在Windows上第一次配置时就是漏了这一步,导致YALMIP一直找不到CPLEX。另外,YALMIP支持的最新CPLEX版本有对应关系,太新的CPLEX配太老的YALMIP会出现内部错误,最好都用当前最新版。

测试一个最小调度模型,能跑通就可以继续:

x = sdpvar(1,1); y = binvar(1,1); Constraints = [0 <= x <= 10, 5*y <= x <= 10*y]; Objective = 2*x + 3*y; sol = optimize(Constraints, Objective, sdpsettings('solver','cplex')); value(x)

看到x=5时,说明环境已经没问题。

3.2 确定性调度建模:先把基准写出来

两阶段鲁棒优化不是一上来就直接写CCG。我的习惯是先建一个确定性模型,把24小时调度解出来,作为鲁棒模型的基准对比,同时验证约束和参数有没有明显错误。

假设微网有2台燃气轮机、1台柴油机、1组储能、光伏和负荷,模型参数用结构体存放:

mpc.T = 24; mpc.gen = [ % 机组: [Pmin Pmax a b] 10 80 0.03 2.1; 20 100 0.025 2.5; 5 40 0.04 1.8 ]; mpc.storage.energy_min = 0; mpc.storage.energy_max = 100; mpc.storage.Pch_max = 20; mpc.storage.Pdis_max = 20;

用YALMIP定义变量:

Pgt = sdpvar(3, mpc.T, 'full'); % 机组出力 Pch = sdpvar(1, mpc.T, 'full'); % 充电功率 Pdis = sdpvar(1, mpc.T, 'full'); % 放电功率 E = sdpvar(1, mpc.T+1, 'full'); % 储能SOC Pbuy = sdpvar(1, mpc.T, 'full'); % 购电 Psel = sdpvar(1, mpc.T, 'full'); % 售电 u_on = binvar(3, mpc.T, 'full'); % 机组状态

再写约束,功率平衡、机组上下限、爬坡、储能SOC递推等。最后设置目标函数:

Obj = sum(sum(mpc.gen(:,3).*Pgt.^2 + mpc.gen(:,4).*Pgt)) ... + sum(2*Pbuy + 0.1*Psel) + 1000*sum(Pcurtail);

注意,燃料成本直接用二次函数会让模型变成二次约束规划,部分求解器支持不好。建议对每台机组用分段线性逼近,或者干脆把成本曲线按几个线性段拆开,引入分段变量。我们在两阶段鲁棒优化里,子问题对偶要求模型是线性规划,所以成本函数必须线性化,这个坑后面会细说。

3.3 手写C&CG:两阶段鲁棒的求解套路

目前求解两阶段鲁棒优化最主流的方法是C&CG,也叫列与约束生成。它在主问题里逐步追加最坏场景对应的约束和第二阶段变量,直到主问题目标值和子问题目标值收敛。比Benders分解更直接,尤其适合第二阶段线性可解的题目。

主问题形式可以写成:

min_x a^T x + η s.t. x ∈ X η ≥ d^T y_k, k=1,...,K A x + B y_k ≥ C u_k + h, k=1,...,K

这里的u_k是第k次迭代时由子问题找出的最坏场景,y_k是新增的场景变量。注意每迭代一次,主问题会新增一块y_k和对应的约束,所以CCG的主问题规模是随迭代次数增长的。

子问题则是固定x,求解:

Q(x) = max_{u∈U} min_{y∈Y(x,u)} d^T y

内层是LP,可以用对偶把min消掉。假设原问题写成 min d^T y, s.t. B y ≥ C u - A x,对偶后变成:

max_{λ≥0} (C u - A x)^T λ s.t. B^T λ ≤ d

这样就变成了max_{u∈U, λ≥0} (C u - A x)^T λ,约束里B^T λ ≤ d。目标里出现u和λ的乘积,也就是双线性项。处理双线性项是子问题求解的关键。

如果U是盒式集合,即每个时段独立满足 u_t ∈ [u_min_t, u_max_t],那么对于目标函数中的每一项,如果把系数提取成关于λ的形式,就能按系数符号直接取u_t的边界。也就是说,最坏场景不需要用优化求解器找,直接对每个时段判断一次符号就可以得到。

实践中,可以先用YALMIP将子问题建模成双线性规划,再调用CPLEX的非线性处理或者用穷举顶点方式求解。但更可靠的做法是:写出内层对偶,然后用枚举方式处理不确定集顶点,这样子问题变成一个纯线性规划,CPLEX直接求解。

CCG的主循环框架如下:

LB = -1e6; UB = 1e6; K = 0; % 初始场景用预测值 u = pv_forecast u_star = pv_forecast; while abs(UB - LB) > 1e-3 && K <= 10 % 求解主问题,包含已发现的场景 sol = optimize(MP_constraints, a'*x + eta, sdpsettings('solver','cplex')); x_star = value(x); LB = value(a'*x + eta); % 固定x,求解子问题,得到最坏场景和成本 [Q_val, u_star, lambda_star] = solve_subproblem(x_star, mp_data); UB = min(UB, value(a'*x_star) + Q_val); % 将新场景u_star加入主问题,新增y_K和约束 K = K + 1; [y_K, MP_constraints] = add_scenario(MP_constraints, u_star, K); end

注意,这里的LB是主问题下界,UB是当前解在最坏场景下的真实成本。因为主问题始终是松弛版,所以LB会一直比真实最优值小;而UB用当前x和子问题计算的成本,是可行解的上界。当两者相近时,说明x已经足够逼近最优。

子问题里如果使用枚举顶点法,需要把不确定集的24个上下界组合算一遍。最朴素的枚举法是2的24次方,显然不现实。好在目标函数关于u的特性表明,如果U是盒式集合且每个时段独立,最坏u_t的选取只取决于对偶变量λ_t的符号,所以只需要一次线性规划,不需要枚举。如果还加了预算约束,那么需要多解一个MILP来挑选最坏u。

3.4 算例参数与结果验证

我给一个简化的典型算例:24小时负荷在80kW到220kW之间波动,光伏预测出力按典型日曲线,预测误差最大20%。两台燃气轮机容量分别是80kW和100kW,储能容量100kWh,最大充放电功率20kW,购电电价采用峰谷分时。

运行CCG迭代,通常在第3次到第6次迭代后上下界差小于1%。确定性模型的总成本假设是4000元,鲁棒模型在最坏情况下的总成本大约到4800元,多出的800元就是为了应对最坏PV偏差所支付的“鲁棒溢价”。这个溢价不是越小越好,而是要和微网的可靠性要求匹配。如果切负荷惩罚设得很高,鲁棒解就会更保守;如果惩罚设得很低,优化器会倾向于允许偶尔切一点负荷来降低总成本。

验证结果是否合理,我一般看三件事。第一,调度结果是否满足功率平衡,逐时段检查净功率是否为0。第二,储能SOC曲线是否在0和最大值之间,且末尾时刻SOC是否等于设定的初始值或结束值。第三,把最坏场景代入确定性方案,看看确定性方案会不会出现约束越界,借此说明鲁棒优化的价值。

4. 常见问题与排查技巧实录

4.1 YALMIP/CPLEX报错速查表

我把实际调试中遇到过的高频报错整理成一张表,方便大家按图索骥。

报错信息常见原因解决办法
Solver not foundCPLEX接口未配置或未安装检查YALMIP路径和CPLEX路径;在MATLAB中运行yalmiptest确认状态
No suitable solver for a problem of this type模型是非线性或非凸,CPLEX不支持将成本函数线性化,或用整数变量重写;检查是否有双线性项未处理
Infeasible problem detected约束过于严格或第一阶段决策导致子问题不可行检查功率平衡约束、机组最小启停约束;调整第二阶段惩罚成本;为子问题增加松弛变量
Index exceeds array bounds变量维度不匹配打印sdpvar的维度,确认所有矩阵大小一致
NaN in objective/constraint参数含NaN或不收敛检查全部输入参数;设置CPLEX迭代上限和MIP gap
Inner problem cannot be solved子问题对偶问题无界或不可行检查第二阶段可行域;增加对偶变量边界;确认强对偶条件成立

我这里特别想提一下“子问题不可行”。两阶段鲁棒优化要求第二阶段对任意u∈U都存在可行y。如果模型里第二阶段约束没有冗余度,比如机组总出力上限刚好等于负荷与最坏光伏的下限,那么在极端场景下就可能出现无解。工程处理手段是在第二阶段目标函数里加很大的切负荷惩罚,本质上是允许物理上少量切负荷,保证始终有可行解。这比一味收紧约束更符合实际运行。

4.2 性能调优:让大模型跑起来

主问题反复增加场景后,MILP规模会变大,单次求解时间可能从几秒涨到几分钟。我常用的调优手段有几个。

首先是给CPLEX设置相对MIP gap。默认是1e-4,对两阶段鲁棒来说太严格了。实际调度问题精度到1%左右就足够,设置成0.01能大幅提速:

settings = sdpsettings('solver','cplex', ... 'cplex.mip.tolerances.mipgap', 0.01, ... 'cplex.timelimit', 300);

其次是给主问题一个初始可行解。用确定性模型算出的x_star作为第一个场景,并把它对应的第二阶段变量加入主问题,冷启动效果好很多。我实验过,一个本来需要迭代5次的模型,用确定性解作为初始场景后,第2次迭代就基本收敛了。

第三是注意大M法参数的取值。处理双线性项或机组状态约束时,M取太小会割掉可行域,取太大会导致数值病态。我通常选1000左右,如果是功率类约束,M取所有机组容量之和的2倍就够了。

4.3 建模正确性检查:别让结果骗了你

鲁棒优化代码跑通只是第一步,结果对不对是另一回事。我一般做三张检查表。

第一,确定性极限检查。把不确定集缩到零(比如ε=0),跑两阶段鲁棒模型,结果应该和确定性模型完全一致。如果不一致,说明主问题或子问题建模有误。

第二,单调性检查。不确定集越大,目标函数值应该越大或至少不降。如果加大预测误差范围后总成本反而下降了,那大概率是子问题取到了错误的场景。可以打印当前x下每个u_t对偶符号,看看是否和预期一致。

第三,人工场景注入。手动构造一个极端场景,比如光伏为零且负荷最高的时段,检验第二阶段能不能通过储能放电和购电补足缺口。如果连最直观的极端情况下都失败,那可能不是优化器的问题,而是约束缺项。

这三步都过了,我对模型基本就有信心了。

5. 后续扩展与实际体会

5.1 从单微网到多微网:扩展方向

这套两阶段鲁棒优化框架不只适用于单个微网。往大了做,可以扩展到多微网互联场景,把微网间的功率交换作为第一阶段签订的交易计划,第二阶段各微网内部再做调整。这种情况下,主问题里会增加微网间联络线潮流约束,子问题也要考虑多个微网同时决策,不确定性可能是光伏和负荷联合随机向量。C&CG框架仍然适用,但子问题会变成一个多主体优化,需要额外处理均衡或合作博弈关系。

往深了做,还可以考虑电氢耦合、电动汽车充放电、需求响应等灵活性资源。两阶段鲁棒优化的优势在于,不管新增多少连续变量和约束,只要第二阶段内层还是线性规划,C&CG的收敛性就有保障。但每加一种元件,子问题对偶的符号和维度就要重新梳理一次,建议先把基础版本的代码模块化,用函数封装机组、储能、联络线,后面扩展只需要写新的元件模块。

5.2 一个过来人的几点体会

我最初尝试两阶段鲁棒优化时,直接抄了一个Benders分解的代码,改了一周都没改明白,后来才发现是子问题对偶中处理双线性项时符号写错了。如果你也是第一次接触这类问题,我的建议是:千万别急着上完整模型,先用3个时段的超简算例把C&CG迭代流程手动跑通,打印每一次的主问题目标、子问题目标、新加入的场景,把每一步都看明白,再扩展到24时段。你花在调试简化模型上的时间,会在后面节省几十倍。

另一个体会是,鲁棒优化不等于越保守越好,也不等于永远选择极端场景。最终决策者关心的其实是“鲁棒溢价”和风险承受能力之间的平衡。你可以通过调整不确定集预算Γ,生成一组成本-风险曲线,让调度人员自己选择运行点。这个比单点计算更能体现两阶段鲁棒优化的工程价值。脚本本身并不复杂,就是在外面包一层循环,逐个Γ跑CCG,把成本曲线画出来。做完这张图,整个项目的说服力会强很多。

最后再分享一个小技巧:输出结果时,把每一阶段的机组出力、储能SOC、最坏不确定场景单独画图,逐条核对。很多模型逻辑错误不是求解器报错,而是结果看起来“正常但不符合物理”。一张好的可视化图能帮你快速定位是哪个约束没生效,比盯着矩阵看有用得多。希望这些踩坑经验能帮你少走点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 18:48:20

ESP-IDF 构建系统:从 idf.py 到组件依赖的实战手册

ESP-IDF 构建系统&#xff1a;从 idf.py 到组件依赖的实战手册 【免费下载链接】esp-idf Espressif IoT Development Framework. Official development framework for Espressif SoCs. 项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf 接手一个 ESP32 项目时…

作者头像 李华
网站建设 2026/9/9 18:47:55

Ruffle 拖放播放:从窗口事件到 SWF 渲染的完整链路

Ruffle 拖放播放&#xff1a;从窗口事件到 SWF 渲染的完整链路 【免费下载链接】ruffle A Flash Player emulator written in Rust 项目地址: https://gitcode.com/GitHub_Trending/ru/ruffle 一个 .swf 文件从资源管理器里被拖进 Ruffle 桌面窗口、松手的那一刻&#x…

作者头像 李华
网站建设 2026/9/9 18:47:09

Python源码导出完全指南:从单文件到环境交付的工程实践

身边经常有朋友问我&#xff1a;Python源码到底怎么导出&#xff1f;一开始我以为他们问的是“把.py文件另存为”&#xff0c;后来发现问的人多了&#xff0c;需求也五花八门——有人想把服务器上跑着的爬虫代码抢救下来&#xff0c;有人想把本地项目完整交接到新电脑上&#x…

作者头像 李华
网站建设 2026/9/9 18:46:51

2024电赛H题自动行驶小车代码解析:STM32状态机与PID控制实战

简介&#xff1a;面向电子设计竞赛参赛者&#xff0c;这份代码包记录了2024年电赛H题的完整实现方案&#xff0c;采用32位微控制器&#xff08;STM32F10x系列&#xff09;进行系统设计&#xff0c;覆盖从电路驱动到上层算法的核心代码&#xff0c;适合备赛学生对照学习、移植与…

作者头像 李华