先问自己一个问题:配电网里装了光伏、储能、充电桩之后,无功电压还压得住吗?我做主动配电网优化调度那段时间,一开始觉得无功优化是个老掉牙的问题,传统配电网里靠有载调压分接头和并联电容器就能解决,直到分布式电源渗透率上来,拓扑从单电源辐射变成多电源双向潮流,电压波动、越限、倒送等问题一下子全冒出来了,这才发现静态无功优化完全不够用,必须把时间维度加进去做动态无功,同时还要对抗分布式电源出力的不确定性——这就是这个项目“主动配电网动态无功两阶段鲁棒优化”要解决的痛点。
这个项目核心就三件事:第一,把无功力优化放进多时段滚动框架,考虑各设备动作成本,避免分接头和电容器频繁操作;第二,用两阶段鲁棒优化对抗光伏和负荷的不确定性,保证最恶劣场景下电压也不越限;第三,用Matlab+YALMIP+商业求解器把整套模型落成可运行的代码,附上Column-and-Constraint Generation迭代框架,解决你“模型会建但代码不会写”的问题。这套东西特别适合正在做配电网方向研究的研究生、刚接触鲁棒优化的工程师,以及想快速跑通一个两阶段鲁棒案例再往深度改论文的同行。
1. 整体设计与思路拆解:为什么非要用两阶段鲁棒
1.1 动态无功解决的“动作次数”难题
所谓动态无功优化,说人话就是把原本一个时间断面的无功优化,扩展成24小时甚至更长时间尺度的多断面联合优化。传统静态优化每个断面独立求解,看着每个时段的电压和网损都挺漂亮,但把结果拉出来看,有载调压变压器可能一小时动作8次,并联电容器一天投切十几回,这些离散设备是有机械寿命的,动作这么频繁,现场根本不敢用。动态优化的本质,就是在目标函数里引入动作成本,用网损和电压偏差去换设备的动作合理性。
我在建模时把设备分了两类:一类是离散设备,包括OLTC档位、电容器组投切,这些动作慢、有机械约束,放到第一阶段,作为一个“here and now”决策提前定下来;另一类是连续可调设备,比如逆变器的无功出力和储能无功,这些响应快,可以放到第二阶段,根据实际光伏出力情况实时调整。这两类设备的物理特征完全不同,正好对应两阶段鲁棒优化的天然分工。
1.2 不确定性的来源与鲁棒化的必要性
光伏出力和负荷预测不可能完全准。我在算例里看到过这样的情况:某天中午预测光伏出力是2MW,实际云层一遮只有1.2MW,如果按预测值去配置无功,电压会往下掉一大截,甚至低于0.95p.u.。你要是做确定性优化,这种场景根本看不见,等现场出了问题再调逆变器,往往已经越限了。
鲁棒优化的思路是不算命,只画圈——我不需要知道光伏出力服从什么概率分布,只需要知道它会落在某个不确定集合内,我做的决策要保证这个集合里任意一个场景都不越限、不失稳。这一点在实际工程里非常实用,因为光伏出力的概率分布很难精确建模,但你告诉运行人员“最差情况我都能扛住”,对方反而容易接受。
1.3 为什么选两阶段而不是单阶段鲁棒
单阶段鲁棒是所有的决策都在不确定性揭示之前做,为了保证可行性,所有设备都必须按照最坏场景一次性定死,做出来的结果会非常保守——明明大多数日子光伏都挺稳,但你要按全年最差云况去配置无功源,光伏逆变器可能长期满发无功,损耗巨大。
两阶段鲁棒的核心区别在于“应对能力”。第一阶段先定OLTC档位和电容器投切这类慢动作设备,第二阶段等光伏出力实际出来后,再用逆变器无功等快动作设备去“接住”不确定性。这样既保证任何场景下都可行,又不会把所有设备都锁死在最悲观的状态。从数学上讲,这就是一个嵌套的min-max-min结构,外层的第一阶段决策求最小成本,中间的不确定变量求最大(最坏场景),内层的第二阶段决策再求最小成本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 两阶段鲁棒优化的数学建模要点
2.1 目标函数与动作惩罚的平衡
目标函数我采用了复合结构:网损成本加电压偏差惩罚加设备动作惩罚。第一阶段的动作惩罚主要是OLTC档位变化和电容器投切次数的绝对值求和,第二阶段的惩罚主要是逆变器无功调整幅值和电压越限的松弛量。这里有个非常关键的经验:动作惩罚的权重不能拍脑袋设,最好先跑一次不加动作成本的多时段优化,统计设备动作次数,然后反推权重。我一开始把动作权重设得很大,结果网损飙升到接近两倍,得不偿失。
最终目标函数的紧凑形式可以写成一个三层的min-max-min表达式,所有设备动作成本都折算成经济成本,用统一的权重系数协调。电压偏差项我用了松弛变量,允许在极端场景下轻微越限但不直接硬约束,这个松弛技巧能显著降低求解难度,而且实际运行时也没人会在意0.002p.u.的瞬时越限。
2.2 DistFlow潮流方程与线性化处理
配电网潮流我用的是DistFlow分支潮流方程,它的好处在配电网上体现得非常充分——辐射状网络下,DistFlow只需要有功、无功、电压幅值三类变量,不需要相角,而且可以通过忽略二阶损耗项直接线性化,形成线性化的DistFlow模型。这个在工程中被大量验证过,在中压配电网里精度完全够用。
线性化之后,节点电压方程可以展开为父支路功率、子支路功率和该节点注入功率之间的关系。有功注入是光伏有功、储能放电减去负荷有功,无功注入是逆变器无功、储能无功、电容器无功减去负荷无功。电容器的无功注入要乘以0/1投切变量,这在第二阶段子问题里会带来整数变量,处理起来要格外小心。我的做法是把电容器投切也放到第一阶段,第二阶段只留连续无功变量,这样能保证子问题是个线性规划,对偶转换和KKT条件处理都方便得多,否则子问题里带整数变量,C&CG主循环很容易出兼容性问题。
2.3 不确定集合的构造与保守度控制
不确定集合我用了盒式加预算约束的组合,这也是文献里最主流的做法。盒式集合描述光伏出力和负荷波动的上下界,预算约束限制同一时刻最多有多少个不确定源可以同时达到最坏值。预算约束的取值直接决定了结果保守程度:预算取0时等价于确定性优化,预算取到不确定源数量时等价于纯盒式鲁棒。我在项目里通常把预算设为不确定源总数的50%到70%,这样既保证鲁棒性又不至于网损过高。
这里有一点需要特别提醒:光伏和负荷的不确定集合要分开建,不要混在一起。光伏的波动是双向的(云遮导致出力降低,但不会超过预测上限),而负荷的波动可以近似认为在预测值附近,所以两者最坏场景的方向可能完全相反。混在一起建集合会把可行域算错,迭代半天都是白算。
3. C&CG求解框架与迭代逻辑拆解
3.1 从min-max-min到主子问题的分解
两阶段鲁棒问题没法直接丢给求解器,必须依靠分解算法。最常用的就是Column-and-Constraint Generation(列与约束生成,C&CG),比传统Benders分解收敛更快,原因在于每轮迭代不仅在主问题里加割平面,还把第二阶段的场景变量作为新列直接加进主问题,相当于主问题在不断地“看到”真实的最坏场景。
主问题是第一阶段决策和已识别出的有限个恶劣场景下的第二阶段决策组成的整体优化问题,它的最优值给原问题提供了一个下界。子问题则在给定第一阶段变量后,去寻找让总成本最高的那个最优变量场景,它的最优值给原问题提供了一个上界(严格说还需要加上第一阶段成本)。两个界不断靠近,小于阈值就收敛。
3.2 子问题的强对偶与线性化处理
子问题内部是一个“给定x后求最大场景再求最小成本”的双层结构,不能直接解。我的做法是走强对偶路线:先把内层min问题写出对偶问题,把max-min变成max-max,合并成一个单层max问题,目标函数里会出现第一阶段决策变量与对偶变量的乘积项,形成双线性项。这一步是新手最容易翻车的地方。
处理双线性项我的经验是按变量类型区分对待:如果配对的变量是连续变量,可以用大M法引入辅助变量实现线性化,M取值需要比变量量纲大两个数量级但别太大,否则数值问题很严重;如果配对的变量里有0/1变量,比如电容器状态,那线性化处理也相对直接,枚举0/1两个取值用大M松弛即可。我建议在写代码前先在纸上把对偶推导完整过一遍,确认每个约束对应的对偶变量都标清楚,再动手写YALMIP,不要一上来就敲代码,排错会排到怀疑人生。
3.3 C&CG迭代终止判据与收敛加速技巧
终止判据我用的是相对间隙:(UB-LB)/abs(UB) <= 1e-4 时停止迭代。这个阈值不是越小越好,太小的阈值会导致迭代次数暴涨,而两阶段鲁棒本身就是个算力黑洞,尤其在33节点系统配合12档OLTC时,纯C&CG迭代20多轮是常事。我实际跑下来发现,阈值设到1e-4和1e-6结果差别在0.1%以内,但迭代轮数差别接近一倍,后来就固定在1e-4。
加速方面有几个小技巧非常管用。第一,给子问题加一个初始可行解,也就是把确定性场景先代入,这样第一轮就有个像样的上界,避免前两轮gap乱跳。第二,在C&CG迭代到后期时,新加入的场景列对主问题的扰动会变小,这时可以用上一轮的主问题解作为热启动,YALMIP里直接通过assign和guess实现。第三,如果光伏节点数特别多,可以考虑把不确定源聚类,用聚类中心代替原始场景集,能大幅缩小不确定集合的显式枚举规模。
4. Matlab代码实现与关键模块解析
4.1 代码总体架构与文件分工
整套Matlab代码我按“数据—建模—迭代—输出”四层拆开,每个文件职责单一,调试和复用都很方便。核心文件包括:主程序入口main_scenario.m负责全局变量初始化和总循环;数据文件load_case33.m定义33节点配电网拓扑、线路参数和负荷曲线;不确定集合生成器build_uncertainty.m构造光伏出力置信区间和预算约束;主问题建模build_master_problem.m和子问题建模build_subproblem.m分别生成YALMIP模型;最后是run_ccg_loop.m实现对偶转换、C&CG迭代和结果汇总。
我一直用YALMIP作为建模层,求解器用Gurobi或CPLEX。这个组合在配电网鲁棒优化领域是事实标准,因为YALMIP对多阶段优化和不确定变量的表达比较友好,而且它可以自动做二次约束转二阶锥,省去很多手写转换的繁重工作。如果你对求解器授权有顾虑,可以用CBC或SCS先跑通流程,但迭代时间会长不少,正式跑算例还是建议Gurobi。
4.2 主问题建模的核心代码框架
主问题在YALMIP里的建模思路是这样:定义第一阶段变量(OLTC档位、电容器投切)、主问题中每一轮迭代对应场景的第二阶段变量、约束条件和目标函数。用循环把每一轮迭代的恶劣场景依次接入主问题,形成“多场景共存的扩展主问题”。
matlab复制% 定义第一阶段变量
tap = binvar(n_tap, T); % OLTC档位,简化用0/1
cap = binvar(n_cap, T); % 电容器组投切状态
% 定义主问题中各迭代场景的第二阶段变量
V = sdpvar(n_bus, T, 'full'); % 节点电压
p_inv = sdpvar(n_pv, T, 'full'); % 逆变器有功
q_inv = sdpvar(n_pv, T, 'full'); % 逆变器无功
q_ess = sdpvar(n_ess, T, 'full'); % 储能无功
Constraints = [];
% 基础潮流约束(线性DistFlow)
for k = 1:iter_count % iter_count为当前C&CG迭代轮数
Constraints = [Constraints, distflow_linear(V(:,:,k), p_inv(:,:,k), ...)];
end
% 设备动作约束
Constraints = [Constraints, abs(tap(:,2:T) - tap(:,1:T-1)) <= 1];
% 目标函数:网损 + 动作惩罚 + 电压越限松弛
Objective = sum(sum(网损项)) + alpha * sum(sum(abs(diff(tap,1,2)))) + ...
这里一定要留意YALMIP里sdpvar和binvar的维度定义,维度写错是运行时报错最频繁的原因,尤其是多维变量的索引方向,YALMIP默认按列展开,跟Matlab的矩阵索引习惯不一致时非常容易搞混。
4.3 子问题与对偶转换的实现细节
子问题是在给定第一阶段变量值之后,求最坏场景下的最小运行成本。实际实现中我不会真的手动写出对偶问题,而是利用YALMIP的dual方法先提取对偶变量,再配合大M法做线性化。但有个坑:YALMIP自动求对偶依赖模型必须是线性的,所以模型里不能出现sdpvar的平方项、绝对值项等非线性表达式,绝对值要用辅助变量和不等式对等价建模。
matlab复制% 子问题:给定tap和cap值
assign(tap, tap_solution); % 从主问题传入第一阶段解
assign(cap, cap_solution);
% 构建子问题模型
Constraints_sub = [...];
Objective_sub = ...;
% 求解子问题,得到最恶劣场景
optimize(Constraints_sub, Objective_sub, sdpsettings('solver','gurobi'));
worst_scenario = value(pv_output); % 提取最恶劣场景
best_cost = value(Objective_sub);
子问题求解完拿到的不仅是最优值,还要把最恶劣场景下的变量取值返回给主问题。这个“场景回传”是C&CG的核心动作,在代码里表现为把worst_scenario作为新的一列追加到主问题的场景列表中。如果漏掉这一步,主问题永远不会感知到恶劣场景,迭代上界下界都不会动。
4.4 参数配置与运行前准备
代码运行前有几项配置必须检查,每一项我都踩过坑。第一,Gurobi的许可证要能被Matlab正常识别,最稳妥的办法是在Matlab里运行gurobi_setup脚本;第二,YALMIP的路径要确保没有跟其他建模工具有路径冲突,老版本YALMIP跟CVX同时出现在path里会互相覆盖函数,报错极其诡异;第三,算例数据的单位要统一,我见过有同行把功率单位混着用,算出来的电压曲线整段偏移,排查了半天才发现是基准容量写错了。
我默认的基准容量是10MVA,基准电压12.66kV,负荷曲线和光伏出力曲线都是按照标幺值给出的,代码里所有的数值运算都要在这个基准下进行。如果换成其他系统,建议先检查网络数据的基准值体系是否一致。
5. 调试过程与常见问题实录
5.1 求解器报错与数值稳定性问题
我印象最深的一次调试,是子问题在对偶转换后报出“模型含有二次等式约束”的错误,原因是某个变量乘积项没被正确大M化,Gurobi直接拒绝求解。后来查出来是大M的系数取小了,导致约束没有正确松弛。大M取值经验是:尽量取约束中变量可能取值的最大上界再乘以50到100倍,不要通篇用同一个大M。比如电压偏差的上界是0.1p.u.,大M取10就够,但如果跟无功功率(上限1Mvar)相乘,大M至少要取50,否则对偶变量的可行域会被错误割掉。
数值稳定性还有一个小细节:Gurobi对线性约束的系数范围较敏感,如果系数从1e-8到1e8都有,求解器数值容差要调整,把NumericFocus设为2或3,能改善大部分数值病态问题。
5.2 迭代不收敛或上下界振荡
C&CG不收敛通常表现为上界下界来回跳,就是gap不是单调下降的。这种问题最常见的根源是子问题里目标函数的符号处理反了,导致“最大化最坏场景”变成了“最小化最坏场景”,场景识别方向反了,主问题加入的场景全是无关场景。
还有一个隐蔽问题:不确定集合的预算约束在子问题里写错了,比如应该约束所有时间断面的不确定源总数,结果代码里写成了每个时刻单独约束,导致最坏场景过于激进,上界突然跳得非常高。排查这类问题的办法是先把预算约束去掉,看确定性场景下上下界是否一致;再加回预算约束,逐步增加不确定源数量,观察gap变化是否平滑。
5.3 求解时间爆炸的定位与应对
两阶段鲁棒优化计算时间长是常态,但如果长到无法接受,就要分层定位瓶颈。我的定位顺序是:第一步看主问题求解时间,如果主问题因为场景列太多而变慢,可以给主问题也加上时间限制,或者在加新场景前先做场景筛选,剔除跟已有场景高度相似的新场景;第二步看子问题求解时间,如果子问题慢,多半是模型非线性太高,检查有没有可以用松弛变量替代的约束;第三步看总迭代轮数,如果超过30轮还不见逼近,建议检查是不是预算约束设定太紧,导致最坏场景几乎每次都在不同方向,主问题始终没见过重复的恶劣场景。
如果实在没办法,可以把问题退化为两阶段随机优化或者场景法,牺牲一部分鲁棒性保障换计算速度,在论文里也可以作为对比方法呈现。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 处理办法 |
|---|---|---|
| 求解器报模型不可行 | 第一阶段变量固定后子问题无解 | 加入电压越限松弛变量,检查OLTC档位连续化处理 |
| 上下界gap不下降 | 子问题最坏场景方向错误 | 检查max/min符号方向与对偶转换推导 |
| 电压曲线有明显突变 | 数据基准值不统一 | 统一标幺值基准并检查线路参数单位 |
| Gurobi报二次约束错误 | 双线性项未正确大M化 | 检查所有乘积项,对应变量取值上界设置大M |
| 目标函数为负且绝对值巨大 | 对偶目标符号反转 | 用确定性场景验证对偶正确性 |
| 迭代轮数极多且每次场景都不同 | 不确定集合预算约束设置不合理 | 减小预算参数,或对不确定源聚类 |
6. 从算例结果到结论扩展的实操心得
最后再分享几个这个项目让我印象最深的实操体会。第一,两阶段鲁棒优化的结果不要只看总成本,一定要把OLTC档位曲线和电容器投切曲线画出来跟确定性优化对比。我见过不止一次,鲁棒优化总成本只高了5%,但设备动作次数少了80%,这才是这个模型真正的价值所在——用少量经济代价换取极大的可控性和安全性。第二,光伏渗透率对鲁棒解的保守性影响非常显著,渗透率从20%升到50%,网损成本可能成倍上升,这时可以适当降低预算约束来平衡,不必死守同一个鲁棒参数。第三,这套两阶段框架不仅适用于无功优化,换一套约束函数就能扩展到电压越限治理、分布式储能配置、甚至配电网灾后重构,我把这个项目的代码改成配电网故障恢复场景只花了不到一周,模型层面的复用性远大于预期。做两阶段鲁棒优化最忌讳的是拿着代码瞎跑不看过程,我强烈建议你先把主问题里的场景个数设为1,复现一个确定性优化作为baseline,再调大不确定集合验证鲁棒解的特性和代价,这样每一步都有对照,代码出了问题也能迅速定位。
