
做电力系统仿真这几年我最常被问到的算法问题就是Matlab里怎么用内点法实现最优潮流。这个词出现的频率几乎和潮流计算一样高尤其在你打开论文看到“OPF”三个字母时后面往往跟着的就是内点法。今天这篇不绕弯直接从数学模型写到优化潮流程序实现把最优潮流问题建模、内点法迭代公式、IEEE 14节点算例和调试心得一次讲透。适合正在做电力系统方向课程设计、研究生课题或者想用Matlab快速验证一个新调度思路的读者前提是你已经对牛顿-拉夫逊潮流计算有基本了解。如果你只会调用现成工具箱看完这篇文章大概也能明白工具背后到底发生了什么。先说结论内点法实现最优潮流没有想象中那么高不可攀。你只需要三样东西一个正确的潮流方程、一个KKT条件的迭代框架、一个牛顿法求解线性系统。所有其他技巧都是围绕这三件事打转。下面我把这套东西拆开讲代码骨架放在中间部分遇到报错的经验放在最后你完全可以按顺序复现也可以直接跳到调试章节找答案。1. 最优潮流的问题建模先把“优化”这件事讲清楚1.1 从潮流计算到最优潮流潮流计算解决的是“给定发电机出力和负荷求全网的电压和功率分布”。它的核心方程是节点功率平衡方程方程数等于系统节点的两倍未知数是电压幅值和相角。潮流计算的输入是一组确定的控制变量输出是一组状态变量没有目标函数也没有不等式约束至少标准潮流计算里不会严格处理运行限值。最优潮流则完全不同。它把潮流计算从“解方程”扩展成“在解方程的同时找最优点”。这里有两组变量控制变量和状态变量。控制变量一般是发电机有功出力、机端电压、变压器抽头等状态变量包括节点电压幅值和相角。优化变量既要让目标函数最优又必须满足潮流方程和各种运行限值。换句话说普通潮流是“给定操作看结果”最优潮流是“反过来问操作怎么定结果才最好”。这个“反过来”就是它难的地方。因为模型的规模会随着节点数增长而且不等式约束数量非常多。比如一个100节点的系统电压幅值有100个上下限发电机有功、无功也都有上下限支路潮流还可能有限制。这些问题叠加起来使得最优潮流的求解难度远高于普通潮流。很多刚开始接触的同学会把最优潮流理解成“潮流计算加一个循环”这是最常见误区。最优潮流的每个迭代步都需要重新线性化本质上是一个大规模非线性规划问题不是几个潮流解里挑一个。1.2 数学模型目标函数、等式约束与不等式约束我习惯把最优潮流写成下面这个标准形式。目标函数一般是最小化系统总发电成本发电成本通常用二次函数近似min sum( a_i * P_Gi^2 b_i * P_Gi c_i )其中P_Gi是第i台发电机的有功出力a_i、b_i、c_i是成本系数。如果没有经济调度需求目标也可以改成最小网损甚至最小电压偏移但程序框架不变。关键点是要把目标函数改成可求导的二次函数内点法对梯度的依赖很重目标函数不可导或者噪声很大收敛性就会很糟糕。实际算例里成本系数单位要统一有人用标幺值有人用有名值单位不一致会导致后面和Matpower对比时怎么都对不上。等式约束就是节点功率平衡方程。对于每个节点i都要满足P_Gi - P_Li - P_i(V, theta) 0 Q_Gi - Q_Li - Q_i(V, theta) 0其中P_Li、Q_Li是负荷P_i和Q_i是由电压幅值和相角决定的节点注入功率。按极坐标写法节点注入功率是电压相量的实部虚部运算具体公式我在后面给代码时会展开。等式约束是必须严格满足的所以它对应拉格朗日乘子。这里有个容易搞错的细节发电机节点和负荷节点都有功率平衡方程但只有发电机节点才有P_Gi和Q_Gi变量。如果程序里把所有节点都统一成同一套变量索引最后雅可比矩阵的行列顺序会乱成一团。不等式约束则多到让你怀疑人生。发电机有功上/下限、无功上/下限、节点电压幅值上/下限、平衡节点出力限制甚至支路潮流限制每条都可以写成x_min x x_max为了统一处理内点法会把它们拆成两个单边不等式并引入松弛变量。每个不等式还需要一个非负条件和互补条件这就是后面KKT条件的来源。建议你先从最基本的“发电机出力限值和电压限值”开始等程序跑通了再逐步加入支路潮流约束。一上来就把所有约束塞进去出错了很难定位是模型问题还是算法问题。1.3 为什么选内点法名字唬人思想朴素我第一次看“内点法”这个名词时以为里面有什么高深的拓扑理论。真正看明白之后发现它的核心思想非常朴素把不等式约束在目标函数后面加一个“会推着你远离边界”的惩罚项让最优解从可行域内部逼近边界。相比简化梯度法内点法通过障碍函数把不等式约束直接包含进拉格朗日函数不需要主动去管哪些约束在起作用。相比遗传算法和粒子群算法内点法每一步都有明确的梯度方向收敛速度有理论保障而且可以处理上千上万变量的问题。现在的很多电力系统商业软件和开源工具比如Matpower里的最优潮流求解器走的也都是内点法路线只是封装成了黑箱罢了。自己动手实现一遍才会把黑箱变成白箱。另外内点法对初始点的要求虽然不像牛顿法那么苛刻但也不是完全无所谓。工程里最常见的做法是先用普通潮流算一个可行解作为初值后面我会专门讲这个操作。选内点法不是因为它最新而是因为它在大规模稀疏问题上表现稳定而且Matlab的稀疏矩阵和线性求解器天然适合实现它。2. 内点法的核心原理照着KKT条件迭代就行了2.1 把不等式约束“塞进”目标函数标准的处理方式是引入松弛变量和障碍函数。假设有一个不等式约束x x_max我写成x s x_max其中s 0。这个s就是松弛变量。然后把障碍项-mu * log(s)加到目标函数中。mu是障碍参数mu越大log项把s往正数方向推得越厉害你不太可能让s碰到0mu越小越允许s接近0也就越接近原来的问题。同理x x_min可以写成x - r x_min其中r 0再加一项-mu * log(r)。把所有障碍项加进去后拉格朗日函数变成L f(x) y*eq z_high*(x - x_max - s) z_low*(x - x_min - r) - mu*(sum(log(s)) sum(log(r)))严格来说还需要对每个等式约束引入拉格朗日乘子对每个不等式约束引入对偶变量。最后的一阶最优性条件就是KKT条件。这个KKT条件是一组非线性方程组我们用的牛顿法就是解这个方程组。你可以把整个过程理解为“把有约束优化问题强行变成一个非线性方程组”剩下的就是机械地做牛顿迭代。2.2 原-对偶内点法的迭代流程很多论文里喜欢写“原-对偶内点法”听起来高级。实际流程就是四个环节先给变量一个初值然后计算当前点的残差再解一个线性化后的方程组得到修正方向最后沿着方向走一步。具体到数学表达式KKT条件可以整理成残差向量r(x,y,z)目标是对每一个变量求偏导使所有偏导数为0。因为这是个非线性系统所以做泰勒展开取一阶项得到线性方程组[K] * [dx; dy; dz] -r其中K矩阵是拉格朗日函数对变量的二阶导数矩阵也就是海森矩阵加上约束的雅可比矩阵组合而成。Matlab里直接可以用dx -K \ r求方向。如果矩阵稀疏Matlab的右除运算符会自动调稀疏求解器效率还不错。但要注意K矩阵通常是对称但不一定正定的矩阵直接使用chol会报错需要用ldl分解或者直接用\。获得牛顿方向后要计算步长。由于松弛变量s和对偶变量z都必须大于0不能一杆子走到头。通常的做法是取所有满足正约束变量中最大的可行步长再乘以0.9995类的系数保证不会恰好撞到边界。否则下一步的log项就无定义了程序直接出现NaN。步长计算是所有调试环节里最容易被忽略的我见过很多人内点法写出来不收敛最后发现是步长直接把变量推到零以下。2.3 关键参数怎么定mu、sigma、步长、容差第一个参数是mu障碍参数初始值一般在0.1量级。如果mu初始太大前面几步非常保守收敛慢如果太小可能一开始就走不到可行域内部。实用中我会用mu 0.1起步之后每次迭代按互补间隙的比例更新。第二个参数是sigma中心参数作用是让迭代路径朝着“减少互补间隙”和“保持中心性”之间平衡一般取0.1到0.2。sigma太小会让路径靠边界sigma太大会收敛慢Matpower内默认也在这个范围。第三个参数是步长乘子我习惯取0.9995也有取0.995的。差一点没关系但不要取1否则下一步log会爆掉。第四个参数是收敛判据看互补间隙gap sum(s .* z) / N_ineq小于1e-5或1e-6就认为收敛。要更严格可以同时看原始残差和对偶残差。这些参数本身不是硬性标准但会影响迭代次数和最终精度。我一般会把每次迭代的gap打到命令窗口看到它单调下降才说明程序没有跑偏。如果gap突然上升或者来回震荡一定要停下来检查不要等到最后发散再处理。2.4 为什么能收敛到最优点牛顿法和KKT的配合这里说点直觉。KKT条件本质上是“在可行域边界附近找一个拉格朗日函数梯度为0的点”。内点法通过障碍项先让你在可行域内部“散步”每次牛顿法都朝满足KKT条件的方向修正。随着mu逐渐减小障碍项变弱路径一步步贴近边界最终落在满足全部约束的最优点上。整个过程等于是把一个有约束优化问题转换成一个带参数mu的无约束问题序列。每个子问题都收敛mu再减小最后总的效果就是收敛到原问题的最优解。这个思路在凸问题上非常稳在电力系统的非凸实际问题中虽然不能保证全局最优但工程上普遍能拿到质量很高的局部最优解。实际项目里一般默认它够用。而且内点法的收敛路径通常是单调的不像智能算法那样需要调一堆种群参数。一旦你理解了“障碍项牛顿法”这个组合就能明白为什么电力系统领域这么多优化问题都愿意用它。3. Matlab实现程序架构和核心函数代码3.1 输入数据组织节点、支路、发电机的向量约定写最优潮流程序最容易乱的地方是变量堆在一起。我的做法是把所有优化变量拼成一个长向量x内部记好索引。比如把向量定义成x [Pg; % 所有发电机有功出力长度ng Qg; % 所有发电机无功出力长度ng Vm; % 所有PQ节点电压幅值长度npq Va; % 所有节点相角长度nb ];具体索引可以在Matlab里用idx结构体存起来不然写到最后一定晕。数据方面我用简化的结构体bus [ % bus_i P_load Q_load Vmin Vmax base_kV ]; gen [ % bus_i Pmax Pmin Qmax Qmin Vtarget a b c ]; branch [ % fbus tbus r x b rate_A tap shift ];如果你熟悉Matpower的mpc结构可以直接从mpc.bus、mpc.gen、mpc.branch里读数据然后转成我这种局部变量。初学者最好是先用一个小系统的数据表比如IEEE 14节点把矩阵格式固定后面换大系统只是改数据文件。变量索引设计有一个小技巧把发电机节点放在节点列表的前面这样潮流雅可比矩阵的分块会更整齐后面调试时看矩阵也方便很多。3.2 潮流注入函数最容易被坑的一步节点注入功率的计算是核心。这里给出一个极坐标形式的函数。注意电压相角最好统一用弧度否则潮流方程和雅可比矩阵全部会错。function [Pcalc, Qcalc] powerInject(Ybus, Vm, Va) nb length(Vm); Pcalc zeros(nb,1); Qcalc zeros(nb,1); for i 1:nb Vi Vm(i); ti Va(i); sumP 0; sumQ 0; for k 1:nb Yik Ybus(i,k); Gik real(Yik); Bik imag(Yik); Vk Vm(k); tk Va(k); delta ti - tk; sumP sumP Vk * (Gik * cos(delta) Bik * sin(delta)); sumQ sumQ Vk * (Gik * sin(delta) - Bik * cos(delta)); end Pcalc(i) Vi * sumP; Qcalc(i) Vi * sumQ; end end写完这个函数一定要单独测试。测试方法很简单随机给一组电压初值再用相同数据跑一遍Matpower潮流计算比较注入功率是不是一致。我这里吃过亏有一次导纳矩阵的符号写反相角差用了tk-ti结果潮流计算的偏差被内点法放大程序直接发散。排查了一个下午才发现是最里层循环的符号错误。所以我建议把这个函数单独存成powerInject.m在任何优化代码之前先跑通。3.3 构建KKT方程组并求解牛顿方向每次迭代都有大量重复计算核心是三步计算目标函数梯度、等式约束残差和雅可比、不等式约束残差以及障碍项相关项。在Matlab里我习惯用结构体存起来res.rg dfdx(x) J_eq * y_lambda J_ineq * z; res.req chareq(x); res.rineq chi(x) - s; res.rz s .* z - mu * ones(nineq,1);其中J_eq是等式约束对x的雅可比矩阵J_ineq是不等式约束对x的雅可比矩阵。KKT线性系统可以写成下面的分块形式用K \ rhs一次解出所有方向[K11 K12; K12 0] * [dx; dlambda] -rhs;这里的K11是拉格朗日函数对x的海森矩阵加对角修正项。如果矩阵奇异说明当前点到不了KKT点常见原因是初值太差或者海森矩阵表达式写错。我建议在调试阶段先用full(K)加det(K)看行列式确认非奇异再上稀疏版本。Matlab里构建海森矩阵时可以利用稀疏矩阵的sparse函数但前提是你已经手动推导出合理的二阶导表达式。如果只是用数值差分小系统可以大系统性能会非常差。3.4 主循环收敛判据与变量更新内点法主循环的骨架我写在这里。为了不把文章变成论文附录我只展示关键逻辑具体残差表达式你要按你自己的变量顺序逐一展开。function [x_opt, f_opt, hist] ipm_opf(x0, opt) x x0; s ones(nineq,1); y ones(neq,1); z ones(nineq,1); mu opt.mu0; gap 1; hist []; for iter 1:opt.maxIter [res, Hess, Jeq, Jineq] calcResiduals(x, s, y, z, mu); K [Hess, Jeq, Jineq; ... Jeq, zeros(neq), zeros(neq,nineq); ... Jineq, zeros(nineq,neq), -diag(z./s)]; rhs -[res.grad; res.eq; res.lower]; dxvec K \ rhs; dx dxvec(1:nx); ds dxvec(nx1:nxnineq); dy dxvec(nxnineq1:nxnineqneq); x x alpha * dx; s s alpha * ds; y y alpha_dual * dy; gap sum(s .* z) / nineq; mu opt.sigma * gap; hist(iter) gap; if gap opt.tol, break; end end end注意这个骨架为了简洁做了简化步长的计算和对偶变量z的更新被省略了。实际你还要计算对偶变量z的方向和更新。如果你想从零开始实现建议先抄一个已经验证过的小规模内点法通用代码对照着改最优潮流模型而不是一上来就写大系统。我自己第一次实现时光步长计算就写了六十行因为要分原始变量、松弛变量、对偶变量三套索引少一套都会出错。3.5 提升效率的三个技巧第一全程用稀疏矩阵。潮流导纳矩阵Ybus用sparse构造变量数多以后效果差距巨大。第二不要在循环里反复计算Ybus的实部和虚部提前缓存成G、B矩阵。第三用符号工具箱求一次导数然后生成函数只用来对拍正式运行时用手写雅可比和二阶项。Matlab符号求导在14节点上还好到了上百节点会慢到怀疑人生。另外每个H2章节建议有小结但我尽量把代码本身说明白。实际优化时我会把目标函数的二阶导数矩阵预先算好每次迭代只用一次函数计算更新而不是重复调用符号工具箱。还有一个容易被忽略的点Matlab的\对稀疏对称矩阵会自动选择求解器但如果KKT矩阵里有大量零块你可以手动排序变量把相同类型的变量放在相邻位置这样矩阵带宽更小求解速度能快不少。4. IEEE 14节点算例实战4.1 算例配置与初值选取我用来测试的是IEEE 14节点标准算例这个系统虽然不大但麻雀虽小五脏俱全发电机有功和无功限值、节点电压上下限、变压器支路都有。我的数据文件里发电机有功单位是MW电压单位是pu相角单位是rad。按照之前说的结构直接读数据后做两件事第一用牛顿法算一次普通潮流把结果作为最优潮流迭代的电压初值第二把发电机出力放在可行域的中间点避免一开始越过限值太多。初值的选取有很大讲究。如果直接用全1电压、全0相角开始内点法可能前几步勉强能走后面就会因为海森矩阵不可逆而崩溃。IEEE 14节点算例还比较温柔你换到IEEE 118节点就体会到了。我每次写新算例都会先用牛顿-拉夫逊跑一次常规潮流再用该解作为初始状态变量剩下的控制变量取上下限中点。这样做以后收敛失败的次数骤降。另外一个细节是发电机无功初值也不要给零最好根据负荷水平给一个中间值否则无功平衡方程很容易在第一步就产生很大的残差。4.2 典型运行结果与边界分析某次典型运行结果我用自己设置的成本系数数值不一定和教材完全一致但趋势有价值如下发电机编号节点有功出力(MW)无功出力(Mvar)G1172.412.7G2242.88.9G3325.76.2G4618.15.6G5812.34.1这个结果的特点是成本较低的机组尽可能多出力成本高的机组压到接近下限节点电压没有越限但有几台机组的出力确实贴着发电机下限。说明在给定的负荷水平下系统存在无功支撑不足的倾向内点法把无功限值变成了起作用约束。系统网损大约在5.2MW左右相比初始可行解的网损下降了约18%。互补间隙从初始的0.5一路降到1e-6迭代次数在23到31之间。这些数字本身不是你复现的重点重点是你会看到不同约束起作用的边界有的约束最后落在slack几乎为零的位置有的约束还有很大余量。建议每轮迭代结束都把slack向量存下来等程序收敛后看一下哪些s接近0这些就是起作用约束也是系统真正紧张的地方。调度人员最关心的其实不是目标函数本身而是哪些设备已经到极限。内点法天然能给出这个信息这一点比很多黑箱求解器友好。4.3 自编程序与Matpower结果对比内点法程序写完后别急着宣布成功。我的验证习惯是用相同数据和相同成本系数调用runopf跑一次再把两个结果放在一起对比。重点对比四样目标函数值、发电机出力、节点电压幅值、迭代收敛次数。如果目标函数差在0.1%以内说明你已经把内点法核心逻辑写对了。如果差异比较大我会先用一个小型3节点系统手推一次KKT条件检查每个残差的符号。这一步虽然枯燥但是排查问题的效率极高。因为自编程序一旦出错很可能出在雅可比矩阵的行列顺序上而这类错误在14节点算例里很难短时间定位。对比Matpower不是为了抄答案而是给自己一个标尺如果连公认结果都对不上后面的安全约束、储能调度统统不可信。我还习惯把每台机组的价格系数打出来确认和Matpower里gencost完全一致。曾经遇到过成本系数单位是$/MWh还是$/pu.h的差异导致目标函数差了好几倍问题不在算法而在数据单位。5. 调试内点法遇到的那些坑5.1 不收敛和振荡的排查思路我的经验是最优潮流程序发散通常从三类根因里来初值不可行、KKT矩阵奇异、步长计算不慎。初值不可行时各残差一开始就很大牛顿方向可能把人带到更远的地方。解决办法就是先跑普通潮流用潮流解做初值或者给控制变量做一次向限值内部的投影。步长计算不慎时gap曲线会上下震荡典型表现是不单调下降。这时候要把步长乘子从0.9995改小一点比如0.995或者干脆在每步对s和z做一次max(val, 1e-8)裁剪防止变量被推到零以下。还有一个很隐蔽的问题是角度基准。潮流方程中相角要相对平衡节点而最优潮流里平衡节点的相角也是一个变量。如果把它固定为0但KKT方程里仍然保留它的位置行列式可能多出一个零特征值。我通常在优化变量里去掉平衡节点相角或者在等式约束里固定它。你可以观察一下KKT矩阵的最小特征值如果有一个接近0的特征值多半就是这类冗余变量导致的。5.2 数值病态、奇异矩阵和NaN处理内点法越接近最优解松弛变量越小KKT矩阵右下角的对角项越病态。但这不意味着你没法解Matlab的ldl分解和mldivide对对称不定矩阵处理得很成熟。我自己的程序里会给海森矩阵的对角线加一个1e-8的微小量类似岭回归防止条件数爆炸。注意这个微小量不能太大否则精度损失不可忽略。NaN出现之后首先要查是不是出现了log(0)或者sqrt(负数)。松弛变量更新后如果被步长乘子压到零下一步的障碍项就炸了。在代码里我习惯加上保护s max(s alpha * ds, 1e-8); z max(z alpha * dz, 1e-8);这个小动作能解决很多莫名其妙的问题。如果你发现NaN只出现在某一次特定约束八成是那个约束的雅可比行列顺序错了而不是数值问题。另一个常见病态来源是变压器支路的变比初值太靠近零导致导纳矩阵里出现异常大的元素。如果Ybus里某一行的数量级和其他行差了好几个量级内点法基本没法收敛。5.3 常见报错速查表报错或症状常见原因处理建议Matrix is singular雅可比行列顺序错误或初值不可行先检查潮流雅可比换初值NaN in objective障碍项出现负数或相角差错误加s、z裁剪检查角度弧度gap不下降或反弹mu、sigma初值不合适降低mu0至0.01减小sigma迭代超过50次收敛判据太严或步长太小调整容差到1e-5步长乘子0.999结果和Matpower差很大成本系数单位不一致统一MW与元/MWh检查基值这张表是我自己在多个算例上的经验汇总。你遇到问题后可以先对着表找方向如果还没解决就用二分法把模型缩小到单台发电机和单个负荷手算验证。小规模问题收敛了再逐步加约束。这样调试的时间成本最低。6. 从这一版程序延伸到更复杂的调度问题6.1 把内点法内核改造成通用优化器如果你把KKT矩阵、残差和步长计算写成独立模块其实你已经拥有一个通用非线性优化内核。后面想换成安全性约束最优潮流只需要把支路潮流不等式约束加进J_ineq想换成考虑储能、抽蓄模型也只需要扩展变量向量和等式约束。我建议在程序结构里把calcResiduals单独放一个文件这样每次调整模型不用动主循环。我自己的ipm_core.m已经连续用了三个项目每次只要改约束或目标函数其余代码一行不动。这里有个很实用的习惯把目标函数、等式约束、不等式约束都写成函数并且规定输入输出格式统一。比如所有约束都返回残差值、雅可比、海森三样东西。虽然前期写起来麻烦但后续扩展省下的时间远超投入。很多论文代码只给一个庞大的脚本改一个变量都要全局搜索这种代码调到后面你会想重写。6.2 从交流最优潮流到直流最优潮流和安全约束扩展如果只是为了日前发电计划这类问题可以先把交流最优潮流简化为直流最优潮流忽略电压和无功只保留有功平衡方程。这时内点法的KKT矩阵规模会小很多收敛也更快。但要注意直流最优潮流的结果不能满足电压安全校核所以工程上通常会做“先DCOPF出计划再交流潮流校验”的两步走。你在自己代码里可以预留一个开关让同一个内点法内核既能跑ACOPF也能跑DCOPF只需在等式约束和变量列表里做条件切换。安全约束扩展也很自然在不等式约束中加入线路传输极限甚至预想事故后的线路潮流约束形成SC-OPF。每加一组约束变量数量会增加KKT矩阵规模变大但对内点法本身来说只是多乘几行没有本质困难。我最近在一个项目里就是在这一版内点法基础上加入了N-1预想事故约束只花很少时间就完成了模型扩展。你会发现自己亲手搭的内点法框架比临时去找一个黑箱求解器更可控因为你知道每一步的KKT残差在说什么。6.3 和智能算法配合的实操边界很多人问内点法能不能和深度强化学习结合。我的想法是真正落地时不要用内点法替代强化学习而是用内点法处理安全校核。比如强化学习负责生成机组组合的试探方案内点法负责在给定组合下快速算出最优潮流和运行成本再把成本和安全信息反馈给学习智能体。这样各司其职比硬让神经网络输出一堆功率值可靠得多。你写的这套内点法代码完全可以被封装成一个带输入输出的函数供主程序反复调用。至少在我接触到的工程中传统优化算法作为底座、智能算法做上层决策的组合比纯端到端方案更容易通过验收。最后有人说内点法这套东西早就该被深度学习或者强化学习替代了。我的态度一直很明确在电力调度这种对可靠性要求极高的场景里需要一个能给出可验证KKT点的传统优化器作为底座再谈智能算法。你自己把内点法亲手实现一遍就能理解为什么很多工程工具至今仍把它当默认选项。这不是守旧是成熟。