做配电网节点电价(DLMP)这块,绕不开三样东西:DistFlow、SOCP松弛、拉格朗日乘子。尤其是MATLAB代码里要同时出现 DLMP、SOCP 和 lindistflow 这三个关键词,说明这已经不是一个花架子算例,而是一套真正面向配网定价的优化模型。
这篇文章我就拿一个典型的配网节点电价项目来复盘,讲清楚 DLMP 为什么不能照搬输电网LMP那套思路,lindistflow 为什么会选 SOCP 而不是简单线性化,以及 MATLAB 里从建模、求解到提取影子价格的完整实操流程。无论你是做配电网规划、分布式资源接入评估,还是研究电力市场节点边际电价,这篇内容都值得认真看一遍。
1. 配电网节点电价为什么不能套用LMP那套思路
1.1 LMP与DLMP:差的不只是电压等级
输电系统的节点边际电价(LMP)大家很熟,本质是安全约束经济调度下的影子价格。只要给定网络拓扑、机组报价和负荷水平,LMP告诉你在某个节点多接入1MW负荷,系统总成本会增加多少。这个逻辑放到配电网,理论框架是成立的,但直接套用会出大问题。
原因在物理特性上。输电网是网状结构,线路电抗X远大于电阻R,有功和无功近似解耦,直流潮流足够描述主要物理关系,所以LMP计算干净利落。配电网以放射状馈线为主,R/X比值高,电阻损耗大,电压约束常常先于线路容量约束被触发。这时候再用直流潮流或忽略网损的模型算节点电价,末端节点的真实稀缺性会被严重低估。
DLMP(Distribution Locational Marginal Price)解决的就是这个问题。它在配电网最优潮流的基础上,不仅考虑能量成本,还能把网损分量、阻塞分量、电压支撑分量拆开。分布式光伏、储能、柔性负荷接入后,配网运维方可以基于DLMP判断“在哪个位置接入资源最划算”,而不是拍脑袋给全配网一个统一电价。
我在实际项目里给一家做分布式聚合商的朋友算过,某条线路末端接入1MW光伏,如果单纯用平均购电价折算,会觉得收益不明显;但把DLMP拆开后发现,末端节点的网损分量和电压分量占了电价的近三成,光伏在末端消纳反而是全局最经济的。这就是DLMP相比传统电价机制的真正价值。
1.2 配网潮流模型选型:牛顿法可以做潮流,做优化却不好用
如果要算DLMP,必然要先解决配网潮流建模问题。最简单的思路是用Newton-Raphson法做完整交流潮流,然后把潮流方程作为等式约束丢进优化模型。听起来直接,实际做起来会撞上一堵墙:这个模型是非凸的。
非凸模型意味着局部最优解和全局最优解可能差得很远,用fmincon这类通用非线性求解器,每次跑出来的结果可能依赖初始点。DLMP本身是要给交易、结算做参考的,今天算一个价、明天换个初值又算出另一个价,这在工程上是不可接受的。更麻烦的是,非凸问题的KKT对偶乘子并不天然具备经济学含义,你很难解释它到底是不是真正的边际价格。
所以配网DLMP领域的通行做法,是找一类凸的潮流近似模型。lindistflow就在这里登场了。它源自DistFlow方程,是专门为放射状配电网设计的精确潮流描述。所谓“linearlized”或者说工程里常说的lindistflow,并不是把问题简单粗暴地砍成线性,而是保留主要物理关系、把非凸项做凸松弛,最终变成一个可以用商业求解器稳定求解的凸优化问题。
这里要特别强调:lindistflow 有两个常用版本。第一类是“无损线性化”,直接丢弃线路损耗项,整个模型变成线性规划,算得快,但网损分量在电价里就完全体现不出来。第二类是“SOCP松弛版本”,也就是标题里那个SOCP,它把损耗相关的二次项放进二阶锥约束里,既保持凸性,又不丢失网损信息。配电网恰好是R比较大的场景,网损对节点电价的影响不可忽略,所以只要你是认真算DLMP,而不是做一个教学演示,SOCP版本几乎是必然选择。
1.3 SOCP松弛是“能算”和“算得对”的分水岭
理解SOCP松弛,关键看DistFlow方程里的非凸项从哪来。
写出放射状配网中一条支路ij的DistFlow方程,形式是这样的:
- 节点注入有功平衡:P_j = P_ij - r_ij * l_ij - ΣP_jk + p_gj - p_dj
- 节点注入无功平衡:Q_j = Q_ij - x_ij * l_ij - ΣQ_jk + q_gj - q_dj
- 电压降落方程:U_j = U_i - 2(r_ij * P_ij + x_ij * Q_ij) + (r_ij^2 + x_ij^2) * l_ij
- 电流定义:l_ij = (P_ij^2 + Q_ij^2) / U_i
前三个方程本身是线性的,问题出在最后一个式子:l_ij 等于功率平方和除以电压,这是一个非凸等式。在处理这个非线性等式时,SOCP的核心技巧是把它放宽为不等式约束:
|| [2P_ij; 2Q_ij; U_i - U_j] ||₂ ≤ U_i + U_j - (r_ij^2 + x_ij^2) * l_ij
这个式子就是二阶锥约束的标准形态。它的几何含义可以这样理解:原本精确的潮流要求功率点必须落在一个曲面上,现在允许它落在一个凸锥体内部。对于放射状网络、目标函数是购电成本最小化这类单调递增函数时,凸优化会自动把解“压回”到锥边界上,也就是松弛是精确的,不会产生物理上不可能的潮流结果。
但这个精确性不是白来的。它依赖于几个条件:配网必须是放射状(或近似树状)、负荷和DG出力在一定范围内、电压不会严重越限。我在代码里用IEEE 33节点系统验证过多次,只要网络数据正常,SOCP求解后检查锥约束的互补间隙,数量级都在1e-6以下,说明松弛精度足够算DLMP。
2. 模型想清楚后,MATLAB代码要这样搭
2.1 目标函数和决策变量的设计逻辑
写MATLAB代码之前,先把数学模型的骨架想清楚,否则代码会越写越乱。
在配电网节点电价问题中,目标函数一般取购电成本最小化,这是目前最主流的做法。假设配网从上级变电站购电,同时配网内部有若干分布式电源(DG),那么目标函数可以写为:
min Σ (c_sub * P_sub) + Σ (c_dg_i * P_dg_i)
其中 c_sub 是主网购电电价,P_sub 是变电站注入有功,c_dg_i 是分布式电源报价,P_dg_i 是各DG出力。这里的目标函数必须是凸函数,SOCP模型要求目标至少是线性的或凸二次的,实际操作中线性目标函数最稳妥。
决策变量需要分三类。第一类是节点变量,包括每个节点的电压平方 U_i,以及节点注入功率 P_i、Q_i;第二类是支路变量,包括每条支路的有功潮流 P_ij、无功潮流 Q_ij 和电流平方 l_ij;第三类才是真正的控制变量,即变电站购电功率和各DG出力。在YALMIP里建变量时,我一般会把变量拆成清晰的block,而不是全部塞进一个大的sdpvar矩阵里,这样后续写约束和提取对偶乘子时思路都会顺很多。
需要提醒的是,很多初学者会把所有节点功率都当作优化变量放开,结果求解器直接给一个荒谬的“增减负荷”方案。正确做法是:除了变电站和DG节点外,普通负荷节点的注入功率是固定参数,不是变量。
2.2 约束体系的组成:潮流、电压、线路容量、DG出力
约束体系是整个模型的核心,我按四个层次来组织代码,每个层次对应一组约束。
第一层:线性潮流约束。对应前面写的DistFlow前三个方程。节点注入有功平衡用YALMIP写出来大概是这样:
% Pij: nb_line x 1, Qij: nb_line x 1 % Ui: nb_bus x 1, lij: nb_line x 1 % 支路i对应from端bus_f(i), to端bus_t(i) for i = 1:nb_line f = bus_f(i); t = bus_t(i); % 电压降落方程 Cons = [Cons, U(t) == U(f) - 2*(r(i)*Pij(i) + x(i)*Qij(i)) ... + (r(i)^2 + x(i)^2)*lij(i)]; end第二层:SOCP锥约束。这一段是模型能否被商业求解器识别为二阶锥的关键。YALMIP里可以直接构造一个旋转二阶锥,更通用的写法是用norm函数构造标准锥:
for i = 1:nb_line f = bus_f(i); t = bus_t(i); cone_expr = [2*Pij(i); 2*Qij(i); U(f) - U(t)]; Cons = [Cons, norm(cone_expr, 2) <= U(f) + U(t) - ... (r(i)^2 + x(i)^2)*lij(i)]; end这里有个非常容易犯的错:有些人图省事,直接把约束写成了双向不等式,也就是把锥约束写成了等式。一旦写成等式,整个模型立刻退化回非凸问题,求解器会报“Nonconvex”或者结果对初始值敏感,这一点我在第四部分会再展开讲。
第三层:运行约束。包括节点电压上下限、支路电流上限、变电站功率上限、DG出力上下限。这些约束大多数是线性不等式,直接加进去即可。电压约束在配网里极其重要,末端电压越限往往比线路过载更早触发,它对应的对偶乘子正是DLMP中电压分量的一部分。
第四层:功率平衡约束。每个节点的有功、无功注入需要满足平衡,也就是对每个节点j,流入功率加DG出力减负荷,必须等于所有流出的支路功率之和加上损耗。这一层约束的对偶乘子,正是我们要提取的节点电价(对于有功),所以建模时这份约束要单独命名,方便后面提取。
所有约束都建完以后,在YALMIP里调用optimize(Cons, Objective, sdpsettings(...))求解,逻辑上就可以运转起来了。
2.3 DLMP的分解与影子价格提取
DLMP并不是一个单一数字,它由多个物理分量叠加而成。在KKT体系中,节点有功平衡等式约束对应的拉格朗日乘子λ_i,就是该节点的总DLMP。它进一步可以分解为:
- 能量分量:全网统一的能量边际价格,由参考节点(变电站)购电成本决定;
- 网损分量:由于线路电阻导致额外能源损耗,反映在电价增量上;
- 阻塞分量:线路功率或电压达到边界时产生的价格差;
- 电压分量(部分文献把它并进阻塞分量):电压约束起作用的节点,乘子会显著抬升电价。
数学上,能量分量对应系统整体平衡约束的乘子,网损分量与支路潮流分布有关,阻塞分量对应线路容量或电压上下限不等式约束的对偶乘子。虽然有些文献给出简洁的闭式分解公式,但在代码里,最直接的工程做法是“先提取总乘子,再用约束边际来拆解分量”。
YALMIP提取节点电价的代码非常简洁:
% 求解完成后提取节点有功平衡约束的对偶乘子 lambda = dual(Cons_balance_p); % 总DLMP向量但这一段有一个大坑:YALMIP的dual()函数只能提取在约束构造时单独命名的约束。如果你把全部约束塞进一个巨大的元胞数组里,YALMIP也能返回对应结构,但稍不留意就会把乘子顺序搞乱。我的习惯是为每一类约束单独定义变量,例如Cons_balance_p专门放有功平衡约束,Cons_balance_q放无功平衡约束,这样提取时怎么都不会错。
还需要注意dual()提取的是对偶乘子的“优化场”意义,符号约定不同文献有不同的习惯。YALMIP对等式约束的dual返回的是拉格朗日乘子λ,通常在最小化问题中,λ_i 的数值就是该节点的DLMP意义下的边际成本。为了保险,你可以先拿一个2节点小系统验证一下:在节点2增加一个很小的负荷扰动,重新求解目标函数增量,和乘子数值做对比,一致就说明符号和数值都对。
3. 33节点算例复现要点
3.1 用IEEE 33节点改装数据的基本流程
理论模型再漂亮,数据不对也白搭。DLMP算例最常见的基础系统是IEEE 33节点配电网,它是放射状结构,本身带12.66kV基准电压、3715kW总负荷,非常接近真实配网特性,而且在公开文献里数据透明,方便对照自己算出的潮流和电价是否合理。
拿到标准33节点数据后,需要做三处改造。第一,设置变电站节点(通常为节点1)的购电成本,我一般取0.5元/kWh量级;第二,在若干节点接入分布式电源,每个DG设置报价,报价一般低于网购电成本,这样才能体现DG的经济价值;第三,把所有有名值参数都换算成标幺值。
标幺值处理这个步骤很多人会忽略,但这是求解器能不能正常收敛的分水岭。比如有名值下线路电阻可能是0.493Ω,电压是12660V,两者量级差好几万,SOCP锥约束里的范数项和电压项数值尺度相差悬殊,求解器很容易在数值上死掉。换算方法很简单:选功率基准值SB=1MVA,电压基准值UB=12.66kV,对应的阻抗基准值ZB=UB^2/SB≈160.3Ω,之后所有电阻电抗都除以ZB,所有功率都除以SB,电压用标幺值(1.0表示12.66kV)。
3.2 构建、求解和结果导出的完整流程
下面是一个可以直接套用的代码骨架,核心部分我用注释标清楚。
% ========== 数据准备:33节点系统(标幺值) mpc = load_case33(); nb = 33; nl = 32; Vmin = 0.95; Vmax = 1.05; Imax = 0.2; % 标幺值电流上限 % ========== 定义变量 U = sdpvar(nb, 1); % 节点电压平方 Pij = sdpvar(nl, 1); % 支路有功 Qij = sdpvar(nl, 1); % 支路无功 lij = sdpvar(nl, 1); % 支路电流平方 P_sub = sdpvar(1, 1); % 变电站购电 P_dg = sdpvar(ndg, 1); % 分布式电源出力(对应接入节点) % ========== 定义约束 Cons = []; % 电压定义约束 Cons = [Cons, U >= Vmin^2, U <= Vmax^2]; % DistFlow线性约束 for i = 1:nl f = mpc.branch(i, 1); t = mpc.branch(i, 2); r = mpc.branch(i, 3); x = mpc.branch(i, 4); Cons = [Cons, U(t) == U(f) - 2*(r*Pij(i) + x*Qij(i)) ... + (r^2 + x^2)*lij(i)]; end % SOCP锥约束 for i = 1:nl f = mpc.branch(i, 1); t = mpc.branch(i, 2); r = mpc.branch(i, 3); x = mpc.branch(i, 4); cone_expr = [2*Pij(i); 2*Qij(i); U(f) - U(t)]; Cons = [Cons, norm(cone_expr, 2) <= U(f) + U(t) ... - (r^2 + x^2)*lij(i)]; end % 节点有功平衡约束(单独命名,用于提取电价) Cons_balance_p = []; for j = 1:nb % inflow项相加, outflow项相加, 等于负荷减注入 % 需要对支路编号做群操作,详细写法见注释文件 end % DG出力约束 Cons = [Cons, P_dg_min <= P_dg <= P_dg_max]; % ========== 求解 ops = sdpsettings('solver', 'mosek', 'verbose', 2, ... 'savesolveroutput', 1); Objective = c_sub * P_sub + sum(c_dg .* P_dg); optimize([Cons, Cons_balance_p], Objective, ops); % ========== 提取DLMP lambda = dual(Cons_balance_p); % 保存结果 dlmwrite('dlmp_result.csv', full(lambda));在求解器选择上,我的建议是:如果有MOSEK或GUROBI,优先选它们,因为这两种求解器对二阶锥规划的处理非常成熟;如果只有MATLAB环境,可以用免费的SDPT3或SeDuMi,但小算例没问题,大算例速度会比较感人。YALMIP会自动识别模型类型,所以不用手动声明“这是SOCP”。
3.3 结果怎么看:DLMP空间分布和分量构成
跑完33节点模型后,把各节点DLMP画在馈线拓扑上,你会直观地看到一条重要规律:距离变电站越远的节点,节点电价通常越高。这不是玄学,因为末端节点每增加1MW负荷,功率要流经更长的馈线,线损增长率更大,同时末端电压更低,可能需要额外的无功支撑,这些都转化为电价的网损分量和电压分量。
从分量拆解的角度看,33节点系统的DLMP构成大体分三种模式。第一种,在馈线中段无阻塞、电压也正常的节点,DLMP主要由能量分量主导,网损分量小,电价接近全网平均水平;第二种,接近馈线末端、尤其是重载线路末端的节点,网损分量会显著上升,电价可能比平均高10%-20%;第三种,如果某个节点附近线路出现了电流越限,阻塞分量会被激活,这时电价会出现一个明显的“台阶式”跳升。
我在实际做这个项目时,特意对比过有DG和无DG两种情况。接入DG后,DG所在节点下游的DLMP会下降,因为本地电源减少了远距离传输功率,网损和阻塞都得到缓解。这个现象特别适合用来做配网规划的“定价引导”:在DLMP高的节点优先鼓励安装分布式储能或光伏,削峰价值最容易被电价信号捕捉。
4. 实战中踩过的坑:调参和排错手册
4.1 求解器报Nonconvex或“非凸问题”怎么办
这个问题在SOCP类项目里出现频率极高,而且往往不是模型真的非凸,而是约束写法导致YALMIP识别错误。
最常见的元凶是锥约束被写成了等式。有些人从公式推导的角度觉得,松弛的最终最优解会在锥边界上,不如直接把它当等式写上去。这个想法看起来合理,实际会把一个凸锥变成一个非凸曲面,YALMIP一旦检测到二次等式,会把模型标记为Nonconvex,直接拒绝调用MOSEK这类凸求解器。
排查方法分两步。第一步,检查约束列表里有没有==号连接二次项表达式;第二步,如果确认是锥约束,用YALMIP内置的check(Cons)函数查看每个约束的“凸性”状态,YALMIP会在求解前返回每个约束是否被识别为凸。
另外要提醒的是,DG成本如果用c_dg * P_dg这种线性报价,没问题;如果写成二次成本,就要保证二次项系数为正,负二次项会让目标函数变成凹函数,再厉害的凸求解器也会直接拒绝。
4.2 对偶乘子符号和提取时间点的问题
乘子符号搞反,是DLMP项目中最容易出的逻辑错误。我之前帮人排查过一个项目,算出来的DLMP全是负的,但系统明明在正常购电。查了半天,发现是YALMIP的等式约束dual返回和用户手动推导的KKT符号约定差了负号。
验证符号有一个很土但绝对有效的办法:对所有节点负荷加一个0.1MW的小扰动,其他不变,重新求解,看目标函数增量,再对比该节点DLMP乘以0.1的值。如果数值一致、方向一致,说明提取的乘子是对的;如果符号相反,就统一取反再进入后续分析。
还有一个细节:dual()返回的对偶乘子必须在optimize()成功求解后立即提取。如果中途把YALMIP变量清空、或者重新构造约束再求解,旧约束对应对偶乘子就失效了。提取出来的乘子建议立刻保存成数组或写入文件,不要等到后面再回头拿。
4.3 数值尺度:从有名值到标幺值的调整
这个问题我再强调一次,因为真的见过太多人在这里卡几天。
配电网的电压是10kV量级,电流是百安培量级,功率是兆瓦量级,支路电阻是欧姆量级。这些量混在一起时,一个SOCP锥约束里既有10^8量级的电压平方项,又有10^0量级的功率乘积项,数值矩阵的条件数会非常糟糕。即便是MOSEK这种稳健的求解器,遇到这种尺度失衡也会出现警告、迭代次数大幅增加,甚至报“Primal Infeasible”或“Numerical Issues”。
标幺化以后,系统内所有量都在0到1.5之间波动,求解器几乎不会遇到数值障碍。这里推荐一个操作习惯:在MATLAB代码里所有变量定义之前,先做完整的基准换算,并把基准值清楚地写在注释里。这样不仅这次求解稳定,后续换一套数据改参数也方便。
4.4 SOCP松弛不紧怎么处理
松弛“不紧”的意思是:模型求出来以后,SOCP锥约束没有取到边界,也就是松弛后的可行域比原物理问题大,最优解落在锥内部,得到的潮流结果和真实潮流有偏差。这种情况在标准配网模型里很少见,但不是绝对没有。
怎么检查?求解后遍历所有支路,计算U(f) + U(t) - (r^2 + x^2)*lij(i)减去norm([2Pij; 2Qij; U(f)-U(t)])的数值偏差。如果偏差量级在1e-5以下,说明松弛精确;如果超过1e-3,就需要警惕。
导致松弛不紧的主要原因通常是约束加错了,例如给某条支路电流上限设了一个非常紧的值,而目标函数又特别想把功率往这条支路上推,两者冲突导致最优解只能停留在锥内部。这时候先检查线路容量参数是否合理,再检查电压下限是否设得太高。数据修正后,松弛通常能恢复紧致。如果确认数据没问题但松弛仍然不紧,一个工程替代方案是用更精确的DistFlow模型或者直接采用交流潮流再验算一遍DLMP。
5. 代码复现后的个人体会和扩展方向
最后分享一点实际感受。DLMP项目如果我第一次接触,很可能被那套对偶乘子和二阶锥的概念绕晕,但真的把代码跑起来后会发现,这个模型最核心的其实就两条线:一条是物理层面的DistFlow,另一条是经济层面的影子价格。SOCP只是连接这两者的桥梁,它的作用是给求解器提供一个凸的、可靠的数学结构。
现在回看这个项目,我最大的收获是明白了“电价不只是价格,更是一组信息”。33节点系统里每个节点的DLMP都告诉你,这个节点在系统当前状态下有多“稀缺”。DG接入也好、储能调度也好,都可以拿这组信息做精细化决策。这比过去那种“全配网统一电价”的方案灵活得多,也科学得多。
后续想继续深入的话,可以考虑三个扩展方向。第一,多时段DLMP,把储能和时移负荷加进来,模型会变成多时段SOCP甚至MISOCP,DLMP会变成随时间变化的分时电价信号;第二,三相不平衡配网的DLMP,这涉及三相DistFlow建模,计算复杂度明显上升,但在实际低压台区很有价值;第三,DLMP的分布式求解,用ADMM把大配网拆成多个区域分别算电价,再用一致性约束协调,这是配网规模化之后的必然需求。
如果你正在照着这个模型调试代码,我的建议是不要急着上大算例,先跑一个3节点或者33节点的小系统,把锥松弛紧致性、乘子符号、网损分量都验证清楚,再逐步扩展到真实馈线数据。基础模型可靠了,后面所有细化方向都顺手。