配电网节点电价(DLMP)这个话题,最近来找我讨论的人明显多了起来。大家的困惑往往很一致:输电网的LMP概念都懂,但一落到配电网,发现既有辐射状拓扑、又有分布式电源、还要处理电压越限,套用输电网那套方法出来的电价怎么看都不合理。我最近刚好把基于lindistflow潮流模型和SOCP二阶锥松弛的MATLAB配网DLMP计算框架完整搭了一遍,从建模、求解到拉格朗日乘子还原都有可复现的代码。这篇就把整个实现思路、关键代码片段和调试中踩过的坑都写出来,给正在做配网节点电价、分布式能源市场机制或主动配电网研究的同学做个参考。
1. 为什么配电网不能照搬输电网LMP:DLMP到底在算什么
1.1 输电网LMP的三个分量及其适用前提
输电网的节点边际电价(LMP)一般拆成能量分量、阻塞分量和网损分量三个部分。能量分量是全系统统一的边际机组报价,阻塞分量反映输电断面受限时不同节点之间的替代困难程度,网损分量则是对潮流损耗的空间归因。这套理论在输电网层面非常成熟,因为它建立在几个关键前提之上:网络呈环网结构、线路R/X比很低、电压问题靠无功调节基本能解耦处理、阻塞断面数量有限且可枚举。
但配电网完全不满足这些前提。配网的R/X比往往在1到3之间甚至更高,电阻损耗占比大,电压问题从线路首端到末端逐级累积;拓扑是辐射状或弱环网,功率流动方向单一但DG接入后可能反转;阻塞不再是少数几个断面,而是大量分支线路的容量约束叠加。更麻烦的是,输电网LMP对网损的处理通常采用网损灵敏度近似,这套近似在配网里误差会大到不可接受。
1.2 配网潮流主导下的价格信号差异
拿一个简单的算例就能说清楚问题。同样一个10kV馈线,从变电站出口到末端负荷,电压降可能超过5%,有功网损在高峰时段占系统总负荷的比例也远高于输电网。在这样的物理场景里,节点价格信号的核心矛盾已经不是"哪个机组最贵",而是"末端节点增加一单位负荷,系统要多付出多少代价"——这里的代价包括额外的网损、可能触发的电压越限风险、以及本已接近满载的支路容量占用。
这就是DLMP(Distribution Locational Marginal Price)要回答的问题。配电网节点电价需要在配网自身的最优潮流模型里定义,通过求解带网络约束的经济调度问题,用节点功率平衡约束的拉格朗日乘子来表征"该节点负荷的边际社会成本"。这个概念在2014年左右开始被集中讨论,这几年随着分布式光伏、储能和车网互动负荷大量接入,实际需求变得非常迫切。
1.3 DLMP的四个分量构成
在配网场景里,DLMP的分析口径通常比输电网多一个电压分量。常规分解方式是:
- 能量分量:系统边际购电成本或边际发电成本,对所有节点基本一致
- 网损分量:节点注入功率变化引起的网络损耗变化,末端节点通常更高
- 阻塞分量:支路容量约束越限时,受限线路上下游节点出现价格差
- 电压分量:电压约束参与调度时,对节点边际价格的附加影响
这四个分量加在一起,才是完整的DLMP。单纯读功率平衡约束的乘子可以得到总和,但要解释"为什么末端节点电价比首端贵两毛钱",必须把分量拆开。后面第3节会讲具体怎么用MATLAB把这四分量还原出来。
2. 模型选型:lindistflow加SOCP为什么是当前最优组合
2.1 DistFlow潮流方程与线性化处理
配网最优潮流的建模,国内文献用得最多的就是DistFlow方程。对辐射状配网,每一条支路k(从节点i流向节点j)满足:
P_ij表示支路有功潮流,Q_ij表示无功潮流。精确的DistFlow方程包含三项:有功平衡、无功平衡、电压方程。精确电压方程是Vj² = Vi² - 2(rP + xQ) - (r² + x²)(P² + Q²)/Vi²,最后一项对应支路电流引起的损耗。这个方程本身是线性方程,但最后一项的分母带Vi²,成了非凸项。
lindistflow的做法是直接丢弃这个高阶小项,令Vj² ≈ Vi² - 2(rP + xQ)。在配网中这个近似的合理性在于:低压配网支路电流不大,损耗项相对2(rP + xQ)通常小一个数量级以上;而且节点电压本身在0.95到1.05 pu范围内,分母项的影响有限。丢掉这一项之后,整个潮流模型变成纯线性约束,配合支路容量约束就可以形成SOCP。
2.2 二阶锥松弛:把非凸约束"鼓成"凸锥
支路容量约束通常写成Pij² + Qij² ≤ Smax²。从几何上看,这个约束在(P, Q)平面上本来是个圆盘(凸集),本身是凸的。真正麻烦的是精确DistFlow里那些包含Vi²、Iij²的项。在lindistflow框架下,如果我们还希望保留电流损耗的间接影响或者处理更精确的线路热极限,就经常碰到的形式是Pij² + Qij² ≤ Vi²·Iijmax²。这个约束里Vi²是变量,把乘积项展开后,可行域不再是凸集。
二阶锥松弛解决的就是这个问题。我们把Vi²单独作为一个变量,记作u_i,于是约束变成Pij² + Qij² ≤ u_i·Iijmax²。这个不等式在数学上等价于(Pij, Qij, (u_i - Iijmax²)/2)这个点落在旋转二阶锥里,YALMIP里可以直接用cone函数表达。
松弛带来的问题是:原本等号成立的约束变成了不等式,可能让最优解"偷懒"抬高电流。但配网辐射状拓扑加上电压幅值作为变量,文献里已经有大量理论证明这种情况下二阶锥松弛是精确的——只要满足正常运行条件和一定的网架条件,松弛的间隙为零。我在实际求解中也基本没有遇到过显著非零的松弛间隙。
2.3 与其他建模方案的对比
这里把几种常见方案放在一起对比,方便你根据自己的场景选型。
| 建模方案 | 潮流精度 | 凸性 | 求解速度 | 适用场景 |
|---|---|---|---|---|
| 直流潮流DC-PF | 很低,忽略无功电压 | 线性规划 | 极快 | 输电网或输配近似 |
| lindistflow+SOCP | 中高,保留无功电压和网损近似 | 二阶锥 | 快 | 配网DLMP、配网调度 |
| 精确DistFlow+SOCP | 高,保留损耗项 | 二阶锥 | 中等 | 高精度配网分析 |
| 全交流AC-OPF | 最高 | 非凸,需要NLP求解器 | 慢且易陷入局部解 | 离线分析,不适合批量计算 |
做DLMP计算我的建议非常明确:用lindistflow加SOCP。原因有三。第一,DLMP的本质是边际价格信号,依赖拉格朗日乘子,这要求模型是凸的,KKT条件才给出全局最优下的对偶变量;第二,配网调度需要反复求解不同负荷场景,求解速度直接决定方案能不能工程化;第三,在0.95到1.05 pu范围内,lindistflow的精度足够支撑价格信号的归因分析,distflow搞复杂了反而让乘子解读变得困难。
2.4 对偶视角:拉格朗日乘子如何变成节点电价
这里讲一个关键的理论问题:为什么节点功率平衡约束的拉格朗日乘子就是节点电价。考虑一个简化的配网经济调度问题,目标函数是购电成本和DG发电成本最小化,约束包括功率平衡、支路潮流、电压上下限和容量约束。构造拉格朗日函数,对节点j的有功平衡约束引入乘子λ_j。
最优解处,如果所有不等式约束都不越限,那么λ_j恰好等于该节点负荷增量的边际成本——因为目标函数对负荷求偏导时,拉格朗日函数中所有包含该节点功率的项都会通过链式法则汇聚到λ_j上。直观理解:节点j多出1MW负荷,系统最优调度需要重新安排发电和潮流分布,这个再调度成本的变化量就是λ_j。
但是注意,这个λ_j已经是包含网损、阻塞、电压影响的综合信号。如果想把四个分量拆开,一般用一个近似分解的做法:在最优解处固定网络状态,分别计算网损灵敏度、阻塞乘子贡献和电压约束贡献,然后用总乘子倒推能量分量。这个方法不完美,但在工程实践中足够稳定,第3.4节会给出具体代码思路。
3. MATLAB工程实现:从数据表到DLMP数值的完整链路
3.1 输入数据组织与拓扑表构建
我用的是IEEE 33节点标准配网算例,改造了几个关键参数:根节点连上级电网,购电电价设为0.5元/kWh;节点18和33接入分布式光伏,发电成本0.35元/kWh,容量各1.5MW和0.8MW。这样构造的目的,是为了同时考察"末端节点高电价"和"DG接入对价格的空间平滑作用"两个现象。
数据组织上,我习惯用两个结构体:bus和branch。bus字段包含节点编号、有功负荷、无功负荷、电压上下限基准值;branch字段包含首端节点、末端节点、支路电阻、支路电抗、容量上限。拓扑关联矩阵在读取branch后自动生成。这里有个小建议:初版代码不要直接用matpower的case文件格式,自己用表格列出来更透明,调试时查错也容易。
3.2 YALMIP变量定义与约束组装
变量定义是整个DLMP实现的骨架。核心变量包括:支路有功潮流、支路无功潮流、节点电压平方、根节点受电功率、各DG有功无功出力、根节点无功受电。用YALMIP的sdpvar定义如下。
% 支路变量 P_br = sdpvar(n_branch, 1, 'full'); % 支路有功潮流 Q_br = sdpvar(n_branch, 1, 'full'); % 支路无功潮流 u = sdpvar(n_bus, 1, 'full'); % 节点电压幅值平方 % 电源变量 P_sub = sdpvar(1, 1, 'full'); % 根节点购电有功 Q_sub = sdpvar(1, 1, 'full'); % 根节点购入无功 P_dg = sdpvar(n_dg, 1, 'full'); % DG有功出力 Q_dg = sdpvar(n_dg, 1, 'full'); % DG无功出力节点功率平衡用关联矩阵搭。关联矩阵B的维度是n_bus乘n_branch,B(i,k)在支路k从节点i流出时为-1,流入时为+1,其余为0。这样一个等式约束就同时覆盖了全部节点:
constraints = []; % 节点有功平衡:B * P_br + 注入电源 = 节点负荷 % 注入电源向量:根节点购电 + DG出力 P_inj = zeros(n_bus, 1); P_inj(sub_idx) = P_sub; for k = 1:n_dg P_inj(dg_bus(k)) = P_inj(dg_bus(k)) + P_dg(k); end constraints = [constraints, B * P_br + P_inj == bus_load];电压方程的约束对应每个支路首末端电压平方差:
% 线性化DistFlow电压方程 for k = 1:n_branch i = branch(k, 1); % from节点 j = branch(k, 2); % to节点 r = branch(k, 3); x = branch(k, 4); constraints = [constraints, u(j) == u(i) - 2*(r*P_br(k) + x*Q_br(k))]; end % 电压上下限 constraints = [constraints, u >= (0.95)^2, u <= (1.05)^2];支路容量约束用YALMIP的cone函数写成二阶锥:
% 支路容量二阶锥约束 for k = 1:n_branch constraints = [constraints, cone([P_br(k); Q_br(k)], S_max(k))]; endcone(x, y)的语义是norm(x, 2) <= y,正好表达支路视在功率不超过S_max。这里S_max的单位需要和P、Q保持一致,通常填MVA。
目标函数为购电成本加DG发电成本:
objective = price_sub * P_sub + sum(price_dg .* P_dg);3.3 SOCP求解与拉格朗日乘子提取
求解配置我用Gurobi作为SDPT3求解SOCP时的对偶问题。注意YALMIP内部会自动把二阶锥问题分派给求解器,不需要额外声明变量类型。求解代码如下:
% 求解SOCP ops = sdpsettings('solver', 'gurobi', 'verbose', 2); result = optimize(constraints, objective, ops);乘子提取是DLMP实现中最关键也最容易出错的一步。功率平衡等式约束的对偶乘子就是该节点的边际价格信号。在YALMIP里,dual函数可以直接取:
% 提取节点有功平衡约束的拉格朗日乘子 lambda_p = dual(constraints(功率平衡约束句柄));但直接这样写有个坑:如果constraints是一个数组,YALMIP的dual返回的是整个约束向量对应的乘子向量,顺序必须和约束定义顺序完全一致。更稳妥的做法是单独保存约束句柄:
% 推荐写法:分离保存关键约束句柄 node_power_constraints = [B * P_br + P_inj == bus_load]; constraints = [constraints, node_power_constraints]; % 求解后提取 lambda_p = dual(node_power_constraints);这里lambda_p向量中的每一个元素就对应一个节点的实时电价(单位为元/MWh)。需要强调,YALMIP返回的乘子遵循其对偶形式约定,正负号可能与KKT教材里的写法相差一个负号,后面第5节会讲如何校验符号方向。
3.4 DLMP四分量还原的实用算法
完整的四分量分解在理论上要构造多层灵敏度分析,工程上我推荐一个三步逼近法,每一步都有明确意义,代码也容易写。
第一步,求总DLMP,也就是直接读lambda_p。这个量一定是对的,因为它源自原始问题的KKT条件。
第二步,把支路容量约束全部拿掉,重新求解一次相同优化问题,再读一次功率平衡乘子,记为lambda_base。这个量对应"没有阻塞时的节点边际价格",它包含了能量分量和大部分网损分量。
第三步,节点j的阻塞分量约等于lambda_p(j) - lambda_base(j)。电压分量的归因可以用对称的做法:把电压上下限约束从1.05/0.95放宽到1.10/0.90,求解后读取lambda_volt_free,DLMP与它的差值可以近似归因于电压约束。
% 三阶段求解函数封装示例 [lambda_total, ~] = solve_dlmp(constraints_full); [lambda_nocap, ~] = solve_dlmp(constraints_without_capacity); [lambda_voltfree, ~] = solve_dlmp(constraints_voltage_loose); dlmp_cong = lambda_total - lambda_nocap; dlmp_volt = lambda_nocap - lambda_voltfree; dlmp_energy_loss = lambda_voltfree;这种方法的好处是不需要解析推导网损灵敏度,纯靠重新求解就能分离出分量,对配网的实际规模完全可以接受的。实测33节点算例整个三阶段求解耗时不到2秒,完全能够支撑多场景批量计算。
4. 算例结果与物理解读:同一馈线上节点电价为何差出一大截
4.1 一个低谷时段的基准DLMP结果
先用一个无DG、负荷为基准值的低谷时段做测试,购电电价0.5元/kWh,所有节点电压和线路容量都未越限。三阶段分解后,典型节点的DLMP如下。
| 节点 | 总DLMP(元/kWh) | 能量+网损分量 | 阻塞分量 | 电压分量 |
|---|---|---|---|---|
| 1(根节点) | 0.501 | 0.501 | 0 | 0 |
| 5(中段) | 0.513 | 0.513 | 0 | 0 |
| 18(末端) | 0.529 | 0.529 | 0 | 0 |
| 33(分支末端) | 0.534 | 0.534 | 0 | 0 |
这个结果非常直观——没有阻塞、没有电压越限时,所有节点的DLMP差异完全来自网损分量。末端节点18比根节点贵约0.028元/kWh,折合成度电成本约每度贵近3分钱,这个量级在真实配电网中完全合理。注意,并不是末端节点本身用电更贵,而是末端增加负荷需要额外的网损成本,DLMP把这个成本精确地归因到了位置信号上。
4.2 阻塞场景下的价格跳变
把节点17到18之间支路的容量上限从5MVA压到2.5MVA,重新求解。结果出现了非常明显的价格解列:支路容量越限,上游节点和下游节点的DLMP之间出现跳变。
| 节点 | 总DLMP(元/kWh) | 阻塞分量 |
|---|---|---|
| 17(阻塞上游) | 0.514 | 0 |
| 18(阻塞下游) | 0.652 | 0.113 |
| 19(下游继续) | 0.658 | 0.119 |
| 20(下游末端) | 0.667 | 0.121 |
阻塞分量在下游所有节点几乎一致出现,幅度约0.11元/kWh,这说明受限支路成了"价格大门",门内门外分别是两套边际成本体系。这个现象在配网实际运行中完全可以观察到——当一个馈线段满载时,下游分布式电源即使成本略高,也会因为替代了昂贵的网损和阻塞成本而变得有竞争力。
4.3 DG接入后的空间平滑效应
同样的阻塞场景,投入节点18的1.5MW光伏,按0.35元/kWh的成本参与调度。DG出力压低了下游负荷需求,支路17-18的潮流下降,阻塞解除。节点18的DLMP从0.652元/kWh回落到0.527元/kWh。这个案例很好地展示了DG的"价格平滑"机制:分布式电源在合适的位置接入,不仅缓解了物理阻塞,还直接把末端节点的电价拉回接近根节点水平。
从这些结果能看出,DLMP不是抽象的数学概念,它就是对物理稀缺性的价格化表达。哪个节点缺调节资源、哪条线路接近极限、哪个DG在关键时刻顶上,都会一字不差地写进节点电价里。
4.4 结果有效性校验方法
拿到一组DLMP数值之后,建议做两个校验。
第一个是方向性校验:在最优解处,把某个节点的负荷增加0.1MW,重新求解SOCP,记录目标函数增量。增量除以0.1应该非常接近该节点的DLMP。这是最直接的经济学验证,误差一般应小于1%。
第二个是KKT残差校验:YALMIP的求解结果里可以读出求解器返回的对偶信息,检查原约束残差和对偶残差是否在容差范围内。Gurobi求解SOCP时默认收敛容差是1e-6,一般不需要手动调整。如果发现KKT残差偏大,优先怀疑变量数值尺度——有功负荷是兆瓦量级,电压平方接近1,两者相差过大时建议把负荷单位统一成千瓦或把目标函数乘以缩放系数。
5. 调试实录:那些让DLMP数值瞬间崩掉的细节
5.1 一次电价符号全反的排查过程
我第一次跑通模型时,输出节点18的电价是负数,-0.15元/kWh。这明显不合理——没有阻塞时末端节点电价怎么都不可能低于根节点购电价。当时第一反应是模型约束错了,但重新检查了功率平衡和电压方程,都没发现问题。
后来用了最原始的办法:逐节点手动算灵敏度。在节点18注入0.1MW,目标函数从原来的值增加了0.0529,这个数值除以0.1正好等于0.529——和理论总DLMP一致。也就是说,求解器给出的最优解和目标函数增量方向完全正确,问题出在dual函数提取的乘子符号上。
排查到这一步就清晰了。YALMIP对等式约束dual返回的乘子,其符号约定是目标函数中约束项加还是减的形式有关。我当时的等式约束写成了B * P_br + P_inj == bus_load,YALMIP默认的对偶形式里返回的乘子对应-lambda * (f(x) - d)这个约定,所以读出来的数值天然就差一个负号。解决办法是用灵敏度校验做一个自动校正:求解完后随机选几个节点测试目标函数对负荷的偏导,确认符号方向后再批量提取。
5.2 dual函数与约束句柄的顺序陷阱
YALMIP的dual函数返回的是约束数组整体对应的向量。如果你用constraints = [constraints, ...]不断拼接约束,之后想提取某一条特定的乘子,YALMIP是按约束在最终拼接对象中的出现顺序排列乘子。这个顺序在调试时极易出错。
我踩过的一个具体问题是:先拼接了电压约束,再拼接功率平衡约束,运行代码时想当然地认为lambda_p是第二个约束的乘子,结果所有价格全都对不上号。更稳的做法是像3.3节那样,为每一类关键约束单独设句柄变量,不混在一起拼接。记住一个原则:乘子提取永远不要依赖顺序,要依赖句柄。
5.3 零负荷节点的边际价格处理
配网算例里常常有零负荷节点(比如联络开关附近的节点,或纯粹为拓扑连接而存在的中间节点)。这些节点的功率平衡等式约束右端是0,拉格朗日乘子依然有定义,但数值往往对求解器数值误差更敏感,偶尔会出现轻微抖动。
处理方法有二。一是在数据预处理时,给孤立零负荷节点加一个极小的基荷(比如1e-4 MW),让它物理上成为一个正常负荷点;二是提取乘子后做中值滤波处理,把明显偏离邻域节点的异常值去掉。工程上第一种方法更干净,不影响价格信号的解释。
5.4 SOCP松弛不精确的识别方法
虽然辐射状配网大多数情况下SOCP松弛是精确的,但求解器数值问题或特殊参数设置有可能让松弛出现非零间隙。判断方法很简单:求解完查看每个支路的二阶锥约束残差,如果norm([P_br(k); Q_br(k)])与S_max(k)之差大于0.1%,说明该支路的锥约束被放松了,结果可能不是严格物理可行的。
为什么会发生?常见原因是S_max填写太大,远超过整条馈线实际可能出现的最大潮流,此时锥约束在最优解处根本不紧,对偶乘子为0,DLMP的阻塞分量自然消失。如果你期望看到阻塞分离,却一直全是零,先检查容量约束是否在可行域边界上起作用。
5.5 一套工程化调试清单
最后整理一份可以直接对照检查的清单,这些都是我在多轮调试中总结出来的高频问题。
- 功率平衡等式约束的右端负荷向量方向是否正确——母线注入为正还是负
- 电压方程中r、x的单位是否与支路潮流变量一致,电阻电抗是每千米值还是全长值
- YALMIP dual提取的乘子是否为求解器约定下的负号形式
- DG出力上限约束的乘子会被YALMIP算进总乘子,注意区分"节点电价"与"机组边际价格"两个概念
- 求解器容差在配网小数值场景下是否需要从默认1e-6调整到1e-8
- 多时段扩展时,储能充放电的跨时段耦合约束会引入时间乘子,与空间DLMP并存后要分开存储和打印
调试DLMP代码的过程,本质上就是把"最优潮流能算出来"升级成"每一个乘子都能解释通"的过程。我个人的经验是,先用灵敏度校验把基准场景的符号和量级对齐,再放开复杂约束,这样每一步出错都能快速定位是建模问题还是求解问题。这套框架你照着搭完,跑通基准场景,再换成自己的配网数据应该就不难了。