MATLAB+CPLEX求解配电网日前调度:二阶锥规划建模全解析
2026/9/9 9:48:03 网站建设 项目流程

前两天帮师弟调一套配电网日前调度的MATLAB代码,核心是用CPLEX求解二阶锥规划模型,把风电(Wind)、并联电容器组(CB)、静止无功发生器(SVG)、有载调压变压器(OLTC)和储能系统(ESS)全部塞进24小时联合优化。问题本身不算复杂,但来来回回折腾了快一天,最后发现是数据维度没对齐导致的报错。这让我想起自己第一次碰这类模型时的情形——光是把每个设备该写到哪个约束、为什么用二阶锥而不是直接解非线性规划,就花了好几周。所以我决定把这套题的建模思路、代码实现和踩过的坑整理出来,既给师弟留个备忘,也方便正被主动配电网调度、电压优化、储能运行这些问题折磨的同学参考。

这篇文章不会只贴一堆代码完事,而是把每个环节的“为什么”讲清楚。你看完以后,应该能自己把模型搭起来,而不是复制别人的代码然后祈祷它能跑。

1. 先搞明白问题:这套模型到底在优化什么

1.1 需求拆解:多设备协同的日前调度

先说题目里的几个缩写。Wind是分布式风电,通常作为有功源接入配电网某个节点;CB是并联电容器组,分组投切,提供无功补偿;SVG是静止无功发生器,能连续调节无功,响应速度快;OLTC是有载调压变压器,通过改变变比来调节电压;ESS是储能系统,既能充电也能放电,具备时间上的转移能力。

为什么要把这五类设备放在一起优化?传统配电网里,无功补偿和电压调节基本是各管各的:电容器手动投切,变压器靠调度员远程调节。但分布式电源大量接入以后,配电网从被动单向网络变成了主动网络,风电出力在一天里波动很大,如果只用单一设备去应对,很容易出现电压越限、无功倒送、设备动作过于频繁等问题。

这套模型解决的正是这个问题:在日前(提前一天)根据预测数据,决定未来24小时每个时段各设备的运行状态。这个“24小时”很关键,因为ESS的荷电状态会跨时段延续,OLTC和CB的动作次数也受机械寿命限制,不能只看单断面,所以必须把时间耦合放进模型。

1.2 为什么选二阶锥规划而不是非线性规划

配电网潮流本质上是非线性的。经典DistFlow支路潮流方程里,电压降和电流平方项会产生非凸约束,直接建模成混合整数非线性规划(MINLP),CPLEX这类商业求解器帮不上忙,自己写算法又很难保证收敛到全局最优。

二阶锥规划的思路是:引入电压幅值平方U和电流幅值平方L两个替代变量,把原本的非凸等式约束松弛成一个凸锥约束。具体来说,支路潮流中有一条约束是

U_i × L_ij = P_ij² + Q_ij²

这是一个旋转二次曲面,非凸。二阶锥松弛把它放宽成

U_i × L_ij ≥ P_ij² + Q_ij²

这个不等式可以用一个标准的旋转锥约束表达,而旋转锥是凸的,CPLEX可以直接处理。

你可能会问:把等式放宽成不等式,结果还可靠吗?这里有个业内都知道的结论:对辐射状配电网,在目标函数是网损最小、购电成本最小这类单调函数的情况下,松弛通常是“紧”的,意思是求解结果中这个不等式会自然取等号,因此解和原问题实际上一致。打个比方,如果某样东西越少越好,你又给它设了一个下限,那它就总会贴着下限走,不会放任自己变大。

这也是为什么这套框架在配电网优化里越来越流行——它有全局最优保证,求解快,还能塞进整数变量(CB、OLTC),论文里写出来也站得住脚。

2. 五大设备逐一建模,细节都在这里

2.1 风电与负荷数据:先把输入做对

模型跑不跑得通,一半以上取决于输入数据组织得对不对。这里说的输入,主要是节点负荷和风电预测出力。

我给你一个可以“抄作业”的构建方式。假设用IEEE 33节点配电网做算例,先把24小时负荷组织成一个矩阵,行是时段、列是节点,每个元素表示该节点该时段的有功负荷。实际算例里通常还会给无功负荷,一般按功率因数0.85到0.95折算即可。构造方式很简单:从典型日负荷曲线(峰平谷三段)提取24个标幺值,再乘上每个节点的峰值负荷。

风电方面,新手最容易犯的错是把风电预测值直接当作必须发出的固定功率。更合理的做法是把它当成“最大可发功率”,引入可弃风变量,约束写成:

0 ≤ P_wind(t) ≤ P_wind_forecast(t)

这样模型可以在系统不需要那么多电时主动弃掉一部分风电,同时在目标函数里给弃风加一个惩罚项。这个处理既贴近实际风电场运行,又不会因为强制消纳导致电压越限或潮流不可行。如果你不想引入可弃风,也可以直接把风电当成负的有功负荷,但那样少了一个对照维度,论文里没得写。

2.2 CB与OLTC:离散变量进入SOCP的两种方式

CB的模型比较简单。电容器组按组投切,每组容量Q_step固定,投入组数是一个整数变量:

Q_cb(t) = n_cb(t) × Q_step

0 ≤ n_cb(t) ≤ n_cb_max

n_cb(t) ∈ Z

这个约束进入模型后是线性的,不会破坏SOCP的凸性,只是让问题从SOCP变成混合整数SOCP,即MISOCP。组数少的时候,也可以把n_cb(t)展开成一组0-1变量,这样更符合CPLEX处理二进制的习惯,但组数多的时候直接用整数变量更省空间。

OLTC的相对麻烦,因为变比和电压是相乘关系,直接乘就非线性了。工程上常见的处理方法是:给变压器支路单独建模,设变比tap(t)是离散档位,取值范围比如0.9到1.1,每档步长0.0125(对应17档)。写约束时可以写成:

U_sec(t) = tap_ratio(t)² × U_pri(t)

tap_ratio(t)是离散变量,但它的平方依然是常数表里查出来的常数,所以这个约束本质上是“常数×变量”的线性形式,不会产生非凸项。还有一种做法是把OLTC等效成一个理想变压器串联一个短路阻抗,在支路上增加一个虚拟节点,两种方式数学上等价,看你自己习惯哪种。

2.3 SVG与ESS:连续调节设备的约束边界

SVG是这里面最简单的设备,它就是节点上一个连续可调的无功源:

-Q_svg_max ≤ Q_svg(t) ≤ Q_svg_max

你只需要把它放在节点无功平衡方程里,不需要引入任何额外变量。有的模型还会给SVG加一个爬坡约束,比如相邻时段无功变化率有限制,实际工程里确实有这种需求,但大多数论文算例里不写,看场景需要。

ESS的建模是重点。它有两个连续变量:充电功率P_ch(t)和放电功率P_dis(t),再加一个状态变量SOC(t)。核心约束是时段间的能量递推:

SOC(t+1) = SOC(t) + η_ch × P_ch(t) × Δt - P_dis(t) / η_dis × Δt

这里η_ch和η_dis分别是充放电效率,典型值在0.9到0.95之间。注意Δt的单位,如果功率是kW、容量是kWh,Δ t必须用小时,比如1小时。很多人在这上面翻车,计算出来SOC一会儿超上限一会儿为负,全是单位换算问题。

另外一个必须加的约束是“不能同时充放电”。从数学上看,如果允许同时充放电,模型会出现一个明显漏洞:储能一边低价充电、一边按放电价卖电,目标函数里形成虚假套利。解决办法是用两个二进制变量:

u_ch(t) + u_dis(t) ≤ 1

0 ≤ P_ch(t) ≤ P_max × u_ch(t)

0 ≤ P_dis(t) ≤ P_max × u_dis(t)

这样充放电状态互斥,物理上才说得通。

2.4 目标函数:不是只有网损一项

很多初学者以为目标函数就是网损最小,真算起来就会发现,光最优化网损,ESS基本不会动作,OLTC和CB也不愿意频繁调节,因为设备动作在目标里没有代价。

真实可用的目标函数通常包含四部分:

  1. 向上级电网购电成本:分时电价 × 根节点注入功率
  2. 网损成本:Σ r_ij × L_ij(t),这个量在SOCP变量里是线性的
  3. 设备动作惩罚:CB投切次数、OLTC挡位变化次数的绝对值之和
  4. 弃风惩罚:弃风量 × 惩罚系数

写成表达式大致是:

min Σ_t [ price(t) × P_sub(t) + c_loss × Σ_ij r_ij L_ij(t) + c_tap × |tap(t+1)-tap(t)| + c_cb × |n_cb(t+1)-n_cb(t)| + c_wind_curtail × P_curtail(t) ]

注意绝对值项不是线性函数,需要引入辅助变量做线性化。比如用Δ_tap(t)表示相邻时段挡位差,加两个不等式约束:

Δ_tap(t) ≥ tap(t+1) - tap(t) Δ_tap(t) ≥ tap(t) - tap(t+1)

然后在目标函数里加c_tap × Δ_tap(t)。这个线性化技巧虽然基础,但很重要,写漏了模型就会变成非线性。

权重系数怎么设?我的经验是:购电成本权重按实际电价,网损成本给一个相对较小的值,设备动作惩罚系数取一个相对值,让模型不会因为频繁调设备而被惩罚,也不会为了省一点点网损就疯狂动作。弃风惩罚系数一般设得比购电价高,这样模型只在必要时才弃风。

3. MATLAB+CPLEX实现过程:从建模到求解

3.1 技术选型:为什么用YALMIP包一层

直接调用CPLEX的MATLAB接口也能写,但要把MISOCP问题转成CPLEX内部的稀疏矩阵结构,写起来极其痛苦,一个规模稍大的模型几百个变量,手写矩阵容易把人写崩溃。

YALMIP是一个MATLAB建模工具箱,作用相当于“翻译官”:你用高级语义把变量和约束声明出来,它自动转换成求解器需要的标准格式,再传给CPLEX。这种方式维护起来方便,改模型也灵活,论文里需要反复调约束的时候优势尤其明显。

安装组合可以参考:MATLAB R2020a及以上 + YALMIP release 2021+ + CPLEX 12.10或20.1。装完之后在MATLAB里运行“yalmiptest”,看到CPLEX被识别为可用求解器就说明环境没问题。

在YALMIP中声明优化问题只需要三样东西:sdpvar(连续变量)、binvar/intvar(二进制/整数变量)、约束列表F,然后调用optimize(F, obj, ops)。

3.2 核心代码骨架:变量声明、约束组装与求解

下面是这套模型的核心代码骨架,我做了简化,但结构是完整的。假设节点数N_bus,支路数N_branch,时段数T=24。

% 1. 变量声明 U = sdpvar(N_bus, T, 'full'); % 节点电压幅值平方 L = sdpvar(N_branch, T, 'full'); % 支路电流幅值平方 Pij = sdpvar(N_branch, T, 'full'); % 支路有功 Qij = sdpvar(N_branch, T, 'full'); % 支路无功 tap = intvar(N_tap, T); % OLTC档位,整数 n_cb = intvar(N_cb, T); % CB投入组数,整数 P_ch = sdpvar(N_bus, T, 'full'); % 储能充电功率 P_dis = sdpvar(N_bus, T, 'full'); % 储能放电功率 SOC = sdpvar(N_bus, T, 'full'); % 储能荷电状态 u_ch = binvar(N_bus, T); % 充电状态 u_dis = binvar(N_bus, T); % 放电状态 % 2. 目标函数 obj = 0; for t = 1:T obj = obj + price(t) * P_sub(t); % 购电成本 obj = obj + c_loss * sum(r_ij .* L(:,t)); % 网损 obj = obj + c_tap * sum(Delta_tap(:,t)); % 挡位动作惩罚 obj = obj + c_cb * sum(Delta_cb(:,t)); % 电容器投切惩罚 obj = obj + c_w * sum(P_curtail(:,t)); % 弃风惩罚 end % 3. 二阶锥约束 F = []; for t = 1:T for ij = 1:N_branch % ||[2P; 2Q; U_i - L_ij]|| <= U_i + L_ij F = [F, cone([2*Pij(ij,t); 2*Qij(ij,t); ... U(from(ij),t) - L(ij,t)], ... U(from(ij),t) + L(ij,t))]; end end % 4. 节点功率平衡、OLTC约束、ESS约束... % (这里省略,按2.1-2.4节的公式展开) % 5. 求解 ops = sdpsettings('solver', 'cplex', 'verbose', 2, ... 'cplex.mip.tolerances.mipgap', 0.001); result = optimize(F, obj, ops); % 6. 输出 if result.problem == 0 U_val = value(U); Pij_val = value(Pij); % 画图、导出表格... else disp('求解失败:' + result.info); end

重点说con ()那一段。YALMIP里con e(x, y)表示约束‖x‖≤y,而旋转锥约束U_i × L_ij ≥ P_ij² + Q_ij²可以通过代数变换写成标准形式:

‖[2P_ij; 2Q_ij; U_i - L_ij]‖ ≤ U_i + L_ij

这段变换是很多人的知识盲区,知道这个写法,二阶锥约束就迎刃而解了。你可以自己验算一下:两边平方,左边展开是4(P²+Q²)+(U_i-L_ij)²,右边是(U_i+L_ij)²,展开后约掉U_i²+L_ij²,就得到U_i × L_ij ≥ P_ij²+Q_ij²。

3.3 24小时数据组织与结果导出

数据组织是实测中最花时间的部分。我的建议是写一个脚本文件单独处理数据,不要什么都堆在主模型里。结构大致是:

  • Load_data.mat:24×N_bus负荷矩阵
  • Wind_data.mat:24×1风电最大出力曲线(标幺或实际值)
  • Price.mat:24×1分时电价
  • Network.mat:节点编号、支路编号、电阻电抗、拓扑连接关系

运行主程序之前,先画一下负荷曲线和风电曲线,肉眼确认趋势合理。这个习惯能省去很多排查故障的时间——我见过有人把负荷曲线乘错了系数,电压约束怎么调都越限,最后发现是数据放大100倍。

求解完之后,用value()提取变量,整理成表格输出。可以在MATLAB里生成类似下面这个结果表:

时段根节点购电功率(kW)ESS放电(kW)CB投入组数OLTC档位最低节点电压(p.u.)
132000430.982
229800430.985
..................

导出表格以后,再画三张图:各时段设备出力堆叠图、24小时电压包络图、储能SOC曲线。这三张图基本是这类论文的标配。

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

4.1 求解器直接报infeasible,怎么找原因

我做仿真最怕见到“infeasible problem”,因为它不告诉你是哪条约束出了问题。根据经验,90%的情况出在以下三处:

一是变量维度不匹配。YALMIP里如果某个变量矩阵声明成N_bus×T,约束里却用到了N_branch×T的维度,模型会直接变成inf feasible,而且报错信息很难定位。排查方法是把每个变量在用到之前的size()函数打出来核对一遍。

二是约束写出来本身矛盾。比如OLTC变比范围太窄,节点电压上下限又卡得很死,两个约束叠加导致没有任何可行解。这个也很常见,风大发时段电压被风电顶上去,OLTC又压不下来,就成这样了。

三是储能SOC末时段约束太苛刻。如果强制要求SOC(24) = SOC(1),但充放电效率和功率限制使得一天内“充回来”的量达不到初值,模型就不可行。解决办法是把末时段约束放宽为SOC(24) ≥ SOC_lower,或者给SOC一个较小的偏差范围。

定位infeasible的方法,我用的最多的是“注释法”:把约束列表从头开始,每跑一次加一组约束,看到底哪组约束加入后问题变成不可行。虽然笨,但有效。“注释法”虽然慢,但配合YALMIP的diagnostics输出,基本一小时之内能找到问题。

4.2 二阶锥松弛不紧,结果不可靠怎么办

这是个学术上更敏感的问题。虽然辐射状配电网的SOCP松弛通常紧,但不是所有场景都保证紧。判断方法很简单:求解后计算所有支路的残差

res_ij(t) = U_i(t) × L_ij(t) - P_ij(t)² - Q_ij(t)²

如果这个值都接近0(比如小于1e-4),说明松弛紧,解是可信的。如果某些时段残差明显大于0,说明模型出现了松弛间隙,结果不等价于原问题。

松弛不紧的常见原因:目标函数里网损权重太小,或者罚项权重设置导致没有动力把潮流推向边界。处理办法有几个:把锥约束右侧的U_i+L_ij稍微乘以一个略大于1的系数;或者给松弛变量加一个小惩罚项;又或者调整目标函数权重,让网损在目标里占更大比例。

要注意的是,这个问题不能靠“把锥约束改回等式”来解决,因为那会重新引入非凸性。正确做法是调整罚项后重算,并在论文里附上残差统计表,让审稿人看到你确实检查过松弛精度。

4.3 求解太慢:MISOCP整数变量太多了

这套模型引入整数变量的地方有CB和OLTC,如果每个时段都用整数变量,总整数变量数是:

T × (N_cb + N_oltc)

24时段、CB接3个节点、OLTC有2台,那就是24×5=120个整数变量,再加上ESS的充放电状态u_ch和u_dis,又是48个二进制变量。CPLEX处理一百多个整数变量的MISOCP通常几十秒能出解,但如果你加的CB组数特别多,或者把设备数量扩大,求解时间会明显上升。

我的经验是三个优化方向:

  • 设相对MIPGap,比如0.001或0.0001,不一定非要0。对工程调度来说,gap在0.1%以内完全可接受。
  • 提供初始可行解。先用连续松弛版本(所有整数变量放松)跑一遍,把结果里整数变量四舍五入后作为整数解的初始值传给CPLEX。
  • 减少整数变量数量。比如CB不按每个节点单独的组数变量,而是用等值容量阶梯方式,把多组投切等价成一个离散档位变量。

sdpsettings里可以这样设置:

ops = sdpsettings('solver','cplex','verbose',2, ... 'cplex.mip.tolerances.mipgap',0.001, ... 'cplex.mip.tolerances.integrality',1e-5);

另外提醒一点,CPLEX的MISOCP求解器中,连续SOCP部分是内点法求解的,warm start不一定总是有效。如果初始解给得不好,反而可能拖慢速度,所以初始解策略要自己对比测试。

4.4 版本兼容与环境问题

YALMIP和CPLEX的版本兼容是个老生常谈的问题,但真的很影响体验。最常见的是YALMIP报“Unknown solver 'cplex'”或者“No suitable solver for this problem”,通常原因是CPLEX的MATLAB接口没有被正常加载。

解决办法:先确认你安装的CPLEX版本里有cplexinteractive和对应MATLAB接口文件夹,然后在MATLAB里运行cplex.setup(Windows下一般是setup_cplex.m)。如果还不行,检查CPLEX版本和MATLAB版本的对应关系。比如R2021b配CPLEX 20.1比较稳,R2018a则建议配CPLEX 12.9。

还有一个细节:如果你的MATLAB是64位,CPLEX也要装64位版本,32位接口装上去能识别但会崩溃。这个坑我踩过一次,报警方式极其诡异——某些模型能解,某些模型一跑就闪退。

5. 扩展思路与实操体会

5.1 从确定性模型走向不确定性优化

这套模型里所有的风电和负荷都用的是预测值,属于确定性优化。如果要做更深入的研究,可以考虑两个方向的扩展。

一个是鲁棒优化。把风电预测误差建模成不确定集合,比如盒式集合或椭球集合,然后求解鲁棒问题。这里要注意,鲁棒对偶转换后模型可能会变成双层结构,处理起来比SOCP复杂很多,但配电网这种规模较小的网络通常还能应付。

另一个是随机规划。用蒙特卡洛生成若干风电场景,每个场景对应一组约束,目标函数变成所有场景的期望成本。这个模型规模会成倍增长,但CPLEX也能处理中小规模的随机MISOCP。

不过我的建议是:先把确定性模型吃透,再把简单的不确定性分析加进去。很多人一上来就想做鲁棒,结果对偶推导错误,最后论文算例根本解释不通。基础模型跑通以后,不确定性扩展就只是工作量问题,而不是难度问题。

5.2 我在反复调试这套模型后的几点体会

第一,一定要先跑单时段、再跑24时段。先固定t=1,把模型简化成单断面优化,确认潮流约束、各类设备约束没问题,再扩展到全天。直接上24时段,数据量一大,报错以后根本分不清是潮流写错还是时序约束写错。

第二,量纲检查要养成习惯。电压标幺值、功率kW/MW、容量kWh这三者之间经常出现数量级不一致的情况。比如电压幅值平方U在标幺值下大约是1.0,而功率可能是几千kW,二阶锥约束里U_i×L_ij这个量纲要跟P²+Q²匹配,数值比例差距太大会让求解器精度崩溃。建议功率全部用标幺值,或者全部用kW和kVar统一,不要在同一个模型里混用。

第三,CB、OLTC这类设备的动作惩罚系数不能设太大也不能设太小。设太大,设备一动不动,电压调节能力闲置;设太小,设备每个时段都在动,机械寿命直接报废。我一般会跑一组参数扫描,看看动作频率随系数的变化曲线,选一个合理的转折点。

第四,这套模型对论文写作帮助很大。MISOCP有全局最优解,不像启发式算法一样需要反复调参数才能保证解的质量,审稿人对这个模型通常比较认可。而且只要把“二阶锥松弛的紧性验证”和“与MINLP方法的对比”两件事做了,文章的理论完整性一下就上去了。

我印象最深的一次调试经历,是某次把所有约束都加齐之后,模型怎么都不可行,我把所有约束逐个注释,折腾了四个小时,最后发现是储能SOC递推公式里效率η放错了位置——应该是充电时乘η、放电时除以η,我写反了。这个错误用眼睛根本看不出来,只有检查约束量纲或者看SOC曲线才会发现:SOC一直在缓慢下降,怎么充都充不满。

所以如果你拿着代码怎么调都不对,不妨先把储能那一段单独拎出来,设一个最简单的场景:给定初始SOC和固定充放电功率,看看SOC曲线是否符合物理直觉。这一类“单元测试”式的验证方法,比死磕整个大模型要高效得多。

下次你再拿到类似的配电网调度题目,按这个顺序走:先理清设备和数据,再写模型约束,接着用小规模算例验证,最后再全时段跑结果。每一步都确认无误,最后的结果基本不会出大问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询