搞配电网分布式优化这行,绕不开ADMM这三个字母。我最早接到"基于串行并行ADMM算法的主从配电网分布式优化控制研究Matlab代码"这个命题的时候,第一反应是找一套能直接跑起来的模型,把串行和并行两种架构都跑通,再回头补理论。结果一上手就发现,论文里轻飘飘的一句"交替方向乘子法",落到真实配电网算例上全是坑:rho调不好残差震荡、parfor看起来并行实际不加速、边界功率符号写反了还给你一个看似收敛的伪解。这篇东西就是冲着这些坑来的,适合那个"凸优化学过、最优潮流会写、想转分布式却看论文看到头大"的研究生或者工程师看。我会把主从配电网为什么非要拆开算、ADMM更新式在代码里长什么样、串行和并行两种实现的本质差异、以及我在标准测试系统上实测下来的收敛行为和参数敏感性,一次性摊开讲清楚。1. 主从配电网为什么要绕开集中式优化1.1 主从结构到底把网络拆成了什么配电网一般是单电源辐射状结构,一个变电站母线往下带若干条馈线,每条馈线再分叉出许多分支。所谓"主从",不是变压器的主备逻辑,而是把整张网络人为划分成一个主区域和若干个从区域。最常用的划分方式是在馈线的某个分段位置断开,把下游一整块负荷连同它内部的分布式电源划成一个从区,上游部分继续作为主区;如果下游还有多个分支,就照这个办法再切出多个从区。以33节点标准辐射状测试系统为例,我习惯选在某条分段支路处断开,切出一个相对独立的下游馈线段作为从区,剩下部分作为主区;再在另一条分支处断开,就变成两个从区。切完之后,主区和从区之间只保留两组物理耦合量:边界节点电压,以及从区从主区吸收的有功和无功功率。分布式优化的全部玩法,就是只交换这两组边界量,每个区域内部的潮流方程各自闭门求解。这种划分不是凭空设计的,现实中它正好对应分段开关和联络开关的位置。地理上本来就存在的开关点,天然就是划分自治范围的边界,通信和采集设备也最容易布置在那些点上。明白这一点之后再去看ADMM里"耦合变量"这个词,就不会觉得它是一个纯数学概念了。1.2 集中式SOCP模型在真实配网里的三个软肋集中式最优潮流非常成熟,DistFlow方程加SOCP松弛之后交给YALMIP调求解器,一个几十节点的配网几分钟就能出结果。但真实配电网落地的时候,集中式有三个绕不开的软肋。第一个是数据汇聚。所有节点负荷、光伏出力的遥测信息必须完整送到控制中心,任何一个远动终端通信中断或者延迟,全局优化要么等、要么直接废。分布式光伏大量接入之后,遥测点数量翻了几倍,这个问题会越来越突出。第二个是隐私边界。配网里可能同时存在多个投资主体,各自的储能、光伏、可调负荷运行数据都是商业信息,没有哪个运营商会愿意把内部出力计划完整交给一个中心。第三个是滚动优化的实时性。系统规模从几十个节点涨到几百个,再叠加大量柔性负荷和分布式电源,集中式模型虽然"能解",但每一次重算都要把全网数据重新刷新一遍,更新周期很难压到分钟级。我不是说集中式要淘汰,而是说在主从结构下做分布式优化,在通信可靠性、利益边界、扩展性上有天然优势。ADMM恰好能把"主从分割"这个物理现实变成一套可收敛的数学算法,这就是为什么这个方向这么多年还有生命力。1.3 ADMM与对偶分解的对比:为什么最后选了它配电网做分布式优化不止ADMM一条路。早期研究者常走对偶分解路线:把全局约束的拉格朗日乘子当作"价格信号"广播下去,各子问题收到价格后独立决策,主问题再根据总量偏差更新价格。这条路思路非常清晰,但致命问题在于:潮流约束带来的目标函数并不强凸,对偶函数一阶梯度更新非常容易抖,经常出现边界量两边来回震荡、就是不收敛的局面。后来大家普遍转投增广拉格朗日方法,靠二次罚项给子问题兜底,ADMM就是这类方法里最适合在Matlab落地的。ADMM的核心优势可以这样理解:每个子问题不是单纯优化自己的目标,它还背着上一轮邻居给的"边界参考值"和一组罚项。罚项让子问题在每一轮保持一定的近似凸性;同时它在同一轮里既更新原始变量、又更新对偶乘子,相当于原始迭代和对偶上升同步进行。对非凸的潮流方程,理论上它没有绝对收敛保证,但大量文献和我的实测都表明,耦合变量选得合理的情况下,它总能落到和集中式几乎一致的运行点上。对做工程和写论文来说,这就足够了。2. ADMM在配网潮流模型中的落地方式2.1 三步更新式:x、z、乘子各司其职标准ADMM求解的问题是:min f(x) + g(z),约束条件是 Ax + Bz = c。写出来的增广拉格朗日先不列了,代码里真正执行的只有三步。第一步,更新x。固定z和乘子λ,求解一个带罚项的局部问题,相当于"在主区域自己的成本和偏离邻居期望之间做权衡":x_new = argmin_x { f(x) + (ρ/2) * || Ax + Bz_old - c + λ_old/ρ ||² }第二步,对称地更新z。这次固定刚算出来的x_new:z_new = argmin_z { g(z) + (ρ/2) * || Ax_new + Bz - c + λ_old/ρ ||² }第三步,更新乘子:λ_new = λ_old + ρ * (Ax_new + Bz_new - c)放到配电网场景里,x可以理解为主区变量,z是从区节点的边界状态;f是主区的网损和运行成本,g是从区的目标函数;而Ax + Bz = c就是我们切分网络时留下的边界一致性约束。每一轮迭代,主从双方都在"用对方上一轮给的边界值修正自己的内部解",一来一回,边界就慢慢对齐了。很多刚接触的人会把这三步当成普通的循环体,实际运行起来才明白:x更新那一步要调用完整的潮流子问题求解器,z更新又要调用一次,乘子更新只是加减法。整个ADMM的算力几乎全花在子问题上,这也是后面并行化能不能提速的关键。2.2 耦合变量选哪些才不会让子问题退化确定边界耦合变量是整个建模里最讲究的一步。最自然也最常用的三组变量是:边界节点电压幅值、主区流向从区的有功功率、无功功率。这三组量在主区那边是支路潮流的末端取值,在从区那边是根节点的注入取值,ADMM的一致性约束把它们拉齐。但这里有个非常容易翻车的地方:如果直接把电压幅值本身作为边界变量,潮流方程里电压和功率是相乘关系,子问题很可能变成非凸,常用求解器会直接抱怨甚至无解。实践经验是把电压平方V²作为一致性变量,DistFlow支路方程在直角坐标下对电压平方的依赖友好得多,子问题的凸性也更可控。另外,不要贪心把主区和从区之间所有支路潮流都设成边界变量。边界变量太多,每个子问题内部真正自由的可调量就太少,解空间被压得厉害,ADMM迭代会像两根弹簧顶着一块板一样反复震荡,半天不收敛。2.3 非凸潮流:子问题用松弛还是直接牛顿法有朋友问我,是不是每个子问题都必须写成SOCP然后调用商业求解器。其实不一定。ADMM的每个子问题里,边界变量被固定后,区域内部就是一个经典潮流可行域问题。你可以用SOCP松弛,也可以保留原始DistFlow方程用牛顿法直接迭代求解。SOCP的好处在于模型化程度高、变量多了不慌;原始牛顿法精度高,但子问题非凸,混合到ADMM的外层迭代里,两层非线性交互会让参数调试变得非常痛苦。我当时在串行版里用的是"SOCP子问题加YALMIP求解",因为实现简单、调试方便;后来要做并行版,发现YALMIP在parfor下面根本跑不快,又花了一个星期把每个子问题重写成二次规划形式,改用Matlab自带的quadprog求解。这样一来每个worker不需要启动解释器建模,循环开销直接降了一个量级。这个选择对工程实现非常关键,第三章我会专门展开。3. Matlab代码框架:串行与并行的实现差异3.1 代码工程的文件划分方式搭建ADMM配网程序,最忌讳把主循环、子问题、拓扑数据全塞进一个巨大的脚本里。我建议按职责拆成目录,调起bug来会感激这个决定:main_serial.m % 串行ADMM入口 main_parallel.m % 并行ADMM入口 data/ case_33bus.m % 节点编号、支路参数、负荷、分布式电源数据 funcs/ build_partition.m % 按指定断点生成主从分区表 solve_sub_master.m solve_sub_slave.m update_multiplier.m calc_residual.m adaptive_rho.m utils/ plot_profile.m % 画电压分布和残差曲线文件拆细之后,每个子函数都能单独测。实际调试中,至少有三分之一的bug出在矩阵维度和符号正负判断上,拆开之后把每个函数的尺寸打出来看一眼就清楚了。我还习惯把节点数据统一放在一个文件里,这样换33节点、69节点系统时只需要改数据文件,主循环逻辑完全不动。3.2 串行ADMM的主循环骨架串行版主循环的框架我写给大家看:lambda = zeros(n_coupling, 1); x_master = initial_master(...); y_slave = initial_slave(...); for k = 1:k_max x_master = solve_sub_master(y_slave, lambda, rho, data); y_slave = solve_sub_slave(x_master, lambda, rho, data); residue = A * x_master + B * y_slave - c; lambda = lambda + rho * residue; [r_pri, r_dual] = calc_residual(x_master, y_slave, lambda, rho); if max(r_pri, r_dual) < tol break; end end有三个细节要提醒。第一,solve_sub_master里使用的是上一轮的y_slave,也就是所谓"旧值",不要在同一个循环内把刚更新过的y_slave再传给master,那就变成另一种迭代格式了。第二,乘子更新的residue是原始残差,检查一下符号是否和一致性约束定义一致。第三,串行版的特点就是信息更新快,x更新完马上影响到y,所以它的迭代次数通常比并行版少三成到五成。3.3 并行ADMM的parfor分工与同步机制并行版和串行版的核心分歧,在从区更新时机。并行版里所有从区在同一个循环轮次内,使用相同的x_master旧值各自独立计算,全部算完之后才汇总。这正好是Matlab parfor的舒适区,因为parfor要求循环迭代互不依赖:parfor s = 1:n_slave y_par(:, s) = solve_sub_slave_par(x_master, lambda_par(:, s), rho, data(s)); end % 同步点:所有从区完成后,统一汇总 y_slave = aggregate(y_par);这里有三个必须注意的坑。第一个,parfor体内不能依赖其他worker的结果,所以每个从区的数据和参数必须提前组织成独立的struct或cell数组,不能共享一个公共矩阵做读写。第二个,如果子问题内部调用YALMIP或者CVX,建模解释过程实际上是单线程跑,parfor开了等于白开,反而多了数据拷贝的额外开销。我把求解器换成了quadprog之后,并行版才真正看到加速。第三个,lambda和ρ是否需要按从区单独维护?我的经验是要。不同从区大小不同、负荷不同,收敛速度也不一样,强制共享同一组乘子会拖慢所有区域。正确做法是lambda做成矩阵,每一列对应一个从区。3.4 残差计算与停止条件的细节残差是ADMM的仪表盘。原始残差反映一致性约束被破坏的程度,对偶残差反映乘子更新是否在来回摆动。只看原始残差、不看对偶残差,你会在某些rho取值下看到原始残差已经不降了但对偶还在猛冲,那种情况是最典型的伪收敛。常用的停止条件带绝对容差和相对容差:eps_pri = sqrt(p) * eps_abs + eps_rel * max(||Ax||, ||Bz||) eps_dual = sqrt(n) * eps_abs + eps_rel * ||A'λ||实操时先设一组严格的eps_abs=1e-5、eps_rel=1e-4跑一遍,看残差的台阶下降形状是否正常,再放宽到研究需要的精度。不要一开始就追求高精度,因为某些数值噪声在极小容差下会变成无限迭代。4. 33节点系统上的收敛结果与参数敏感性4.1 测试环境与基准解为了把串行和并行两个版本的差异说清楚,我在一台普通的8核CPU、16GB内存笔记本上跑了33节点标准测试系统,节点电压基准12.66kV,总负荷约3.7MW加2.3Mvar无功。网络切分成一个主区和两个从区:在一条主干支路处断开形成从区1,在另一条分支处断开形成从区2,耦合变量大约是三组边界量的组合。基准解用集中式SOCP最优潮流得到,目标函数是系统网损加少量DG成本惩罚项,所有分布式方案都拿这个解当标尺。4.2 迭代次数、墙钟时间与目标值对比稳定测试的一组参数是rho=0.8、eps_abs=1e-4、eps_rel=1e-3,结果如下:版本完成迭代次数单次平均耗时总墙钟时间与集中式目标偏差串行ADMM260.42s10.9s0.06%并行ADMM480.18s8.6s0.12%集中式SOCP1—2.1s—单看迭代次数,串行优势非常明显,因为它每一轮都让从区立刻吃到主区最新信息;但看总耗时,并行反而更快,原因是子问题经过quadprog重写后,计算瓶颈全在矩阵运算,多核分担那三个子问题能实打实把墙钟时间拉下来。如果从区数少于CPU核数,或者子问题求解还是一层YALMIP解释器包着,串行就会反超。这个结论直接决定你代码里该选哪个分支。4.3 rho对收敛性的影响与自适应策略rho这个参数对ADMM收敛速度的影响,只能用"极其敏感"来形容。rho太小,乘子更新像蜗牛爬,原始残差倒是稳,但迭代次数可能从20次飙升到90多次;rho太大,比如取到10,前几轮原始残差确实飞速下降,但之后再怎么迭代都进不了容忍度,因为对偶残差开始高频震荡,整个算法在边界附近来回弹。后来我在代码里加了自适应rho策略,核心逻辑很简单:每5轮比较一次原始残差和对偶残差的比值,比值大于10就放大rho,比值小于0.1就缩小rho,其余情况保持不变。用了这个策略之后,rho初值随便在0.01到10之间取,都能在40轮以内收敛。这对参数扫描实验是救命级别的优化,不然每组rho都要手动调半天。if mod(k, 5) == 0 ratio = r_pri / r_dual; if ratio > 10 rho = rho * 2; elseif ratio < 0.1 rho = rho / 2; end end5. 调参走过的弯路与可复用建议5.1 从残差不降到收敛的完整排查顺序跑出来残差不降,第一个动作不是改rho,而是按下面这个顺序查。第一步,打印每个子问题解向量,看边界变量的值是否在动。如果一动不动,问题几乎一定出在子问题求解器本身的可行域定义上。第二步,检查一致性约束矩阵的符号。主区流出功率的正方向,在从区那边必须反号,这是整个建模里最容易错的地方。我有一次就是符号写反,ADMM迭代出的电压曲线整体往下平移了一段,表面看残差嗡嗡往下掉,实际是伪收敛。第三步,把最大迭代次数设到300,打印每50轮的残差值,判断是发散还是单纯收敛慢。如果是发散,先把rho降一个数量级;如果是收敛慢,优先查eps_rel是不是设置得太严。排查顺序的价值在于,它让你用最少的时间确定是"模型错了"还是"参数没调好"。这两个问题处理方式完全不同,一个是改代码,一个是改配置。5.2 parfor加速的收益边界在哪里我在前文反复强调过,YALMIP子问题在parfor下很难提速。为了让自己死心,我用profile工具统计过:单次子问题求解里,YALMIP建模解释开销占了70%以上,真正优化器求解只占不到30%。当瓶颈在建模解释这一层时,开再多worker也只是反复触发解释器,收益微乎其微。把子问题重写成quadprog之后,建模开销几乎消失,耗时集中在矩阵分解和回代,多核并行的价值才真正释放。还有个偷时间的小技巧:如果并行的几个从区拓扑结构完全相同、只是负荷场景不同,那么每个从区的常数矩阵可以先做一次因数分解并缓存,每次迭代只做回代。这个优化在串行和并行版里都好用,单次子问题耗时能省一半以上。5.3 正确性验证的三板斧验证ADMM写没写对,我固定做三件事。第一,把收敛后的节点电压曲线和集中式解画在同一张图里,两条线应几乎重叠。第二,换两批不同的初始lambda跑两次,看最终解是否基本不变。如果初始值不同结果就不同,那说明算法很可能落在非凸问题的某个局部KKT点上,需要换更小的rho或者重新考虑是否该把更多的非凸约束放进去。第三,单独测试每个子问题自身的KKT条件:随机给一组边界变量,检查子问题内部的约束残差是否真的为0。这三项全过,代码才能放心拿去跑实验和出图。5.4 把这套框架往三相不平衡和微网群扩展这套主从ADMM的可扩展性比想象中好。要改成三相不平衡配网,变量从电压平方标量变成三相向量,耦合变量从3个变成9个,子问题的维度相应膨胀,但框架不用推翻。要接入微网群,把每个微网当成一个从区,边界设在公共连接点,加一组换流器控制变量就能描述功率互济。我做过的测试里,改动都集中在子问题建模和一致性约束的维度上,协调层的主循环几乎不用动。最后分享一个工作习惯:每次跑完一组参数,把残差曲线、目标值、迭代耗时、rho轨迹全部存成一个mat文件,文件名带上场景和日期。写论文做敏感性分析的时候,直接回头翻历史结果,不用一切重跑。这种小事在项目后期能省出好几天时间。用一句话总结目前的体会:串行ADMM赢在迭代次数少、逻辑直观,适合当研究的"第一版基线";并行ADMM赢在墙钟时间短、扩展性强,但要真正发挥优势,必须绕开YALMIP那层解释器开销。两个版本都留在工程里,比纠结谁替代谁有用得多——它们解决的是研究过程中不同阶段的问题。如果你正准备搭这套代码,我的建议是先把串行跑顺,子问题和残差逻辑全部验证干净,再动手并行版。先吃到ADMM的"稳",再去追它的"快",这条路最顺。
搞配电网分布式优化这行,绕不开ADMM这三个字母。我最早接到"基于串行并行ADMM算法的主从配电网分布式优化控制研究Matlab代码"这个命题的时候,第一反应是找一套能直接跑起来的模型,把串行和并行两种架构都跑通,再回头补理论。结果一上手就发现,论文里轻飘飘的一句"交替方向乘子法",落到真实配电网算例上全是坑:rho调不好残差震荡、parfor看起来并行实际不加速、边界功率符号写反了还给你一个看似收敛的伪解。这篇东西就是冲着这些坑来的,适合那个"凸优化学过、最优潮流会写、想转分布式却看论文看到头大"的研究生或者工程师看。我会把主从配电网为什么非要拆开算、ADMM更新式在代码里长什么样、串行和并行两种实现的本质差异、以及我在标准测试系统上实测下来的收敛行为和参数敏感性,一次性摊开讲清楚。1. 主从配电网为什么要绕开集中式优化1.1 主从结构到底把网络拆成了什么配电网一般是单电源辐射状结构,一个变电站母线往下带若干条馈线,每条馈线再分叉出许多分支。所谓"主从",不是变压器的主备逻辑,而是把整张网络人为划分成一个主区域和若干个从区域。最常用的划分方式是在馈线的某个分段位置断开,把下游一整块负荷连同它内部的分布式电源划成一个从区,上游部分继续作为主区;如果下游还有多个分支,就照这个办法再切出多个从区。以33节点标准辐射状测试系统为例,我习惯选在某条分段支路处断开,切出一个相对独立的下游馈线段作为从区,剩下部分作为主区;再在另一条分支处断开,就变成两个从区。切完之后,主区和从区之间只保留两组物理耦合量:边界节点电压,以及从区从主区吸收的有功和无功功率。分布式优化的全部玩法,就是只交换这两组边界量,每个区域内部的潮流方程各自闭门求解。这种划分不是凭空设计的,现实中它正好对应分段开关和联络开关的位置。地理上本来就存在的开关点,天然就是划分自治范围的边界,通信和采集设备也最容易布置在那些点上。明白这一点之后再去看ADMM里"耦合变量"这个词,就不会觉得它是一个纯数学概念了。1.2 集中式SOCP模型在真实配网里的三个软肋集中式最优潮流非常成熟,DistFlow方程加SOCP松弛之后交给YALMIP调求解器,一个几十节点的配网几分钟就能出结果。但真实配电网落地的时候,集中式有三个绕不开的软肋。第一个是数据汇聚。所有节点负荷、光伏出力的遥测信息必须完整送到控制中心,任何一个远动终端通信中断或者延迟,全局优化要么等、要么直接废。分布式光伏大量接入之后,遥测点数量翻了几倍,这个问题会越来越突出。第二个是隐私边界。配网里可能同时存在多个投资主体,各自的储能、光伏、可调负荷运行数据都是商业信息,没有哪个运营商会愿意把内部出力计划完整交给一个中心。第三个是滚动优化的实时性。系统规模从几十个节点涨到几百个,再叠加大量柔性负荷和分布式电源,集中式模型虽然"能解",但每一次重算都要把全网数据重新刷新一遍,更新周期很难压到分钟级。我不是说集中式要淘汰,而是说在主从结构下做分布式优化,在通信可靠性、利益边界、扩展性上有天然优势。ADMM恰好能把"主从分割"这个物理现实变成一套可收敛的数学算法,这就是为什么这个方向这么多年还有生命力。1.3 ADMM与对偶分解的对比:为什么最后选了它配电网做分布式优化不止ADMM一条路。早期研究者常走对偶分解路线:把全局约束的拉格朗日乘子当作"价格信号"广播下去,各子问题收到价格后独立决策,主问题再根据总量偏差更新价格。这条路思路非常清晰,但致命问题在于:潮流约束带来的目标函数并不强凸,对偶函数一阶梯度更新非常容易抖,经常出现边界量两边来回震荡、就是不收敛的局面。后来大家普遍转投增广拉格朗日方法,靠二次罚项给子问题兜底,ADMM就是这类方法里最适合在Matlab落地的。ADMM的核心优势可以这样理解:每个子问题不是单纯优化自己的目标,它还背着上一轮邻居给的"边界参考值"和一组罚项。罚项让子问题在每一轮保持一定的近似凸性;同时它在同一轮里既更新原始变量、又更新对偶乘子,相当于原始迭代和对偶上升同步进行。对非凸的潮流方程,理论上它没有绝对收敛保证,但大量文献和我的实测都表明,耦合变量选得合理的情况下,它总能落到和集中式几乎一致的运行点上。对做工程和写论文来说,这就足够了。2. ADMM在配网潮流模型中的落地方式2.1 三步更新式:x、z、乘子各司其职标准ADMM求解的问题是:min f(x) + g(z),约束条件是 Ax + Bz = c。写出来的增广拉格朗日先不列了,代码里真正执行的只有三步。第一步,更新x。固定z和乘子λ,求解一个带罚项的局部问题,相当于"在主区域自己的成本和偏离邻居期望之间做权衡":x_new = argmin_x { f(x) + (ρ/2) * || Ax + Bz_old - c + λ_old/ρ ||² }第二步,对称地更新z。这次固定刚算出来的x_new:z_new = argmin_z { g(z) + (ρ/2) * || Ax_new + Bz - c + λ_old/ρ ||² }第三步,更新乘子:λ_new = λ_old + ρ * (Ax_new + Bz_new - c)放到配电网场景里,x可以理解为主区变量,z是从区节点的边界状态;f是主区的网损和运行成本,g是从区的目标函数;而Ax + Bz = c就是我们切分网络时留下的边界一致性约束。每一轮迭代,主从双方都在"用对方上一轮给的边界值修正自己的内部解",一来一回,边界就慢慢对齐了。很多刚接触的人会把这三步当成普通的循环体,实际运行起来才明白:x更新那一步要调用完整的潮流子问题求解器,z更新又要调用一次,乘子更新只是加减法。整个ADMM的算力几乎全花在子问题上,这也是后面并行化能不能提速的关键。2.2 耦合变量选哪些才不会让子问题退化确定边界耦合变量是整个建模里最讲究的一步。最自然也最常用的三组变量是:边界节点电压幅值、主区流向从区的有功功率、无功功率。这三组量在主区那边是支路潮流的末端取值,在从区那边是根节点的注入取值,ADMM的一致性约束把它们拉齐。但这里有个非常容易翻车的地方:如果直接把电压幅值本身作为边界变量,潮流方程里电压和功率是相乘关系,子问题很可能变成非凸,常用求解器会直接抱怨甚至无解。实践经验是把电压平方V²作为一致性变量,DistFlow支路方程在直角坐标下对电压平方的依赖友好得多,子问题的凸性也更可控。另外,不要贪心把主区和从区之间所有支路潮流都设成边界变量。边界变量太多,每个子问题内部真正自由的可调量就太少,解空间被压得厉害,ADMM迭代会像两根弹簧顶着一块板一样反复震荡,半天不收敛。2.3 非凸潮流:子问题用松弛还是直接牛顿法有朋友问我,是不是每个子问题都必须写成SOCP然后调用商业求解器。其实不一定。ADMM的每个子问题里,边界变量被固定后,区域内部就是一个经典潮流可行域问题。你可以用SOCP松弛,也可以保留原始DistFlow方程用牛顿法直接迭代求解。SOCP的好处在于模型化程度高、变量多了不慌;原始牛顿法精度高,但子问题非凸,混合到ADMM的外层迭代里,两层非线性交互会让参数调试变得非常痛苦。我当时在串行版里用的是"SOCP子问题加YALMIP求解",因为实现简单、调试方便;后来要做并行版,发现YALMIP在parfor下面根本跑不快,又花了一个星期把每个子问题重写成二次规划形式,改用Matlab自带的quadprog求解。这样一来每个worker不需要启动解释器建模,循环开销直接降了一个量级。这个选择对工程实现非常关键,第三章我会专门展开。3. Matlab代码框架:串行与并行的实现差异3.1 代码工程的文件划分方式搭建ADMM配网程序,最忌讳把主循环、子问题、拓扑数据全塞进一个巨大的脚本里。我建议按职责拆成目录,调起bug来会感激这个决定:main_serial.m % 串行ADMM入口 main_parallel.m % 并行ADMM入口 data/ case_33bus.m % 节点编号、支路参数、负荷、分布式电源数据 funcs/ build_partition.m % 按指定断点生成主从分区表 solve_sub_master.m solve_sub_slave.m update_multiplier.m calc_residual.m adaptive_rho.m utils/ plot_profile.m % 画电压分布和残差曲线文件拆细之后,每个子函数都能单独测。实际调试中,至少有三分之一的bug出在矩阵维度和符号正负判断上,拆开之后把每个函数的尺寸打出来看一眼就清楚了。我还习惯把节点数据统一放在一个文件里,这样换33节点、69节点系统时只需要改数据文件,主循环逻辑完全不动。3.2 串行ADMM的主循环骨架串行版主循环的框架我写给大家看:lambda = zeros(n_coupling, 1); x_master = initial_master(...); y_slave = initial_slave(...); for k = 1:k_max x_master = solve_sub_master(y_slave, lambda, rho, data); y_slave = solve_sub_slave(x_master, lambda, rho, data); residue = A * x_master + B * y_slave - c; lambda = lambda + rho * residue; [r_pri, r_dual] = calc_residual(x_master, y_slave, lambda, rho); if max(r_pri, r_dual) < tol break; end end有三个细节要提醒。第一,solve_sub_master里使用的是上一轮的y_slave,也就是所谓"旧值",不要在同一个循环内把刚更新过的y_slave再传给master,那就变成另一种迭代格式了。第二,乘子更新的residue是原始残差,检查一下符号是否和一致性约束定义一致。第三,串行版的特点就是信息更新快,x更新完马上影响到y,所以它的迭代次数通常比并行版少三成到五成。3.3 并行ADMM的parfor分工与同步机制并行版和串行版的核心分歧,在从区更新时机。并行版里所有从区在同一个循环轮次内,使用相同的x_master旧值各自独立计算,全部算完之后才汇总。这正好是Matlab parfor的舒适区,因为parfor要求循环迭代互不依赖:parfor s = 1:n_slave y_par(:, s) = solve_sub_slave_par(x_master, lambda_par(:, s), rho, data(s)); end % 同步点:所有从区完成后,统一汇总 y_slave = aggregate(y_par);这里有三个必须注意的坑。第一个,parfor体内不能依赖其他worker的结果,所以每个从区的数据和参数必须提前组织成独立的struct或cell数组,不能共享一个公共矩阵做读写。第二个,如果子问题内部调用YALMIP或者CVX,建模解释过程实际上是单线程跑,parfor开了等于白开,反而多了数据拷贝的额外开销。我把求解器换成了quadprog之后,并行版才真正看到加速。第三个,lambda和ρ是否需要按从区单独维护?我的经验是要。不同从区大小不同、负荷不同,收敛速度也不一样,强制共享同一组乘子会拖慢所有区域。正确做法是lambda做成矩阵,每一列对应一个从区。3.4 残差计算与停止条件的细节残差是ADMM的仪表盘。原始残差反映一致性约束被破坏的程度,对偶残差反映乘子更新是否在来回摆动。只看原始残差、不看对偶残差,你会在某些rho取值下看到原始残差已经不降了但对偶还在猛冲,那种情况是最典型的伪收敛。常用的停止条件带绝对容差和相对容差:eps_pri = sqrt(p) * eps_abs + eps_rel * max(||Ax||, ||Bz||) eps_dual = sqrt(n) * eps_abs + eps_rel * ||A'λ||实操时先设一组严格的eps_abs=1e-5、eps_rel=1e-4跑一遍,看残差的台阶下降形状是否正常,再放宽到研究需要的精度。不要一开始就追求高精度,因为某些数值噪声在极小容差下会变成无限迭代。4. 33节点系统上的收敛结果与参数敏感性4.1 测试环境与基准解为了把串行和并行两个版本的差异说清楚,我在一台普通的8核CPU、16GB内存笔记本上跑了33节点标准测试系统,节点电压基准12.66kV,总负荷约3.7MW加2.3Mvar无功。网络切分成一个主区和两个从区:在一条主干支路处断开形成从区1,在另一条分支处断开形成从区2,耦合变量大约是三组边界量的组合。基准解用集中式SOCP最优潮流得到,目标函数是系统网损加少量DG成本惩罚项,所有分布式方案都拿这个解当标尺。4.2 迭代次数、墙钟时间与目标值对比稳定测试的一组参数是rho=0.8、eps_abs=1e-4、eps_rel=1e-3,结果如下:版本完成迭代次数单次平均耗时总墙钟时间与集中式目标偏差串行ADMM260.42s10.9s0.06%并行ADMM480.18s8.6s0.12%集中式SOCP1—2.1s—单看迭代次数,串行优势非常明显,因为它每一轮都让从区立刻吃到主区最新信息;但看总耗时,并行反而更快,原因是子问题经过quadprog重写后,计算瓶颈全在矩阵运算,多核分担那三个子问题能实打实把墙钟时间拉下来。如果从区数少于CPU核数,或者子问题求解还是一层YALMIP解释器包着,串行就会反超。这个结论直接决定你代码里该选哪个分支。4.3 rho对收敛性的影响与自适应策略rho这个参数对ADMM收敛速度的影响,只能用"极其敏感"来形容。rho太小,乘子更新像蜗牛爬,原始残差倒是稳,但迭代次数可能从20次飙升到90多次;rho太大,比如取到10,前几轮原始残差确实飞速下降,但之后再怎么迭代都进不了容忍度,因为对偶残差开始高频震荡,整个算法在边界附近来回弹。后来我在代码里加了自适应rho策略,核心逻辑很简单:每5轮比较一次原始残差和对偶残差的比值,比值大于10就放大rho,比值小于0.1就缩小rho,其余情况保持不变。用了这个策略之后,rho初值随便在0.01到10之间取,都能在40轮以内收敛。这对参数扫描实验是救命级别的优化,不然每组rho都要手动调半天。if mod(k, 5) == 0 ratio = r_pri / r_dual; if ratio > 10 rho = rho * 2; elseif ratio < 0.1 rho = rho / 2; end end5. 调参走过的弯路与可复用建议5.1 从残差不降到收敛的完整排查顺序跑出来残差不降,第一个动作不是改rho,而是按下面这个顺序查。第一步,打印每个子问题解向量,看边界变量的值是否在动。如果一动不动,问题几乎一定出在子问题求解器本身的可行域定义上。第二步,检查一致性约束矩阵的符号。主区流出功率的正方向,在从区那边必须反号,这是整个建模里最容易错的地方。我有一次就是符号写反,ADMM迭代出的电压曲线整体往下平移了一段,表面看残差嗡嗡往下掉,实际是伪收敛。第三步,把最大迭代次数设到300,打印每50轮的残差值,判断是发散还是单纯收敛慢。如果是发散,先把rho降一个数量级;如果是收敛慢,优先查eps_rel是不是设置得太严。排查顺序的价值在于,它让你用最少的时间确定是"模型错了"还是"参数没调好"。这两个问题处理方式完全不同,一个是改代码,一个是改配置。5.2 parfor加速的收益边界在哪里我在前文反复强调过,YALMIP子问题在parfor下很难提速。为了让自己死心,我用profile工具统计过:单次子问题求解里,YALMIP建模解释开销占了70%以上,真正优化器求解只占不到30%。当瓶颈在建模解释这一层时,开再多worker也只是反复触发解释器,收益微乎其微。把子问题重写成quadprog之后,建模开销几乎消失,耗时集中在矩阵分解和回代,多核并行的价值才真正释放。还有个偷时间的小技巧:如果并行的几个从区拓扑结构完全相同、只是负荷场景不同,那么每个从区的常数矩阵可以先做一次因数分解并缓存,每次迭代只做回代。这个优化在串行和并行版里都好用,单次子问题耗时能省一半以上。5.3 正确性验证的三板斧验证ADMM写没写对,我固定做三件事。第一,把收敛后的节点电压曲线和集中式解画在同一张图里,两条线应几乎重叠。第二,换两批不同的初始lambda跑两次,看最终解是否基本不变。如果初始值不同结果就不同,那说明算法很可能落在非凸问题的某个局部KKT点上,需要换更小的rho或者重新考虑是否该把更多的非凸约束放进去。第三,单独测试每个子问题自身的KKT条件:随机给一组边界变量,检查子问题内部的约束残差是否真的为0。这三项全过,代码才能放心拿去跑实验和出图。5.4 把这套框架往三相不平衡和微网群扩展这套主从ADMM的可扩展性比想象中好。要改成三相不平衡配网,变量从电压平方标量变成三相向量,耦合变量从3个变成9个,子问题的维度相应膨胀,但框架不用推翻。要接入微网群,把每个微网当成一个从区,边界设在公共连接点,加一组换流器控制变量就能描述功率互济。我做过的测试里,改动都集中在子问题建模和一致性约束的维度上,协调层的主循环几乎不用动。最后分享一个工作习惯:每次跑完一组参数,把残差曲线、目标值、迭代耗时、rho轨迹全部存成一个mat文件,文件名带上场景和日期。写论文做敏感性分析的时候,直接回头翻历史结果,不用一切重跑。这种小事在项目后期能省出好几天时间。用一句话总结目前的体会:串行ADMM赢在迭代次数少、逻辑直观,适合当研究的"第一版基线";并行ADMM赢在墙钟时间短、扩展性强,但要真正发挥优势,必须绕开YALMIP那层解释器开销。两个版本都留在工程里,比纠结谁替代谁有用得多——它们解决的是研究过程中不同阶段的问题。如果你正准备搭这套代码,我的建议是先把串行跑顺,子问题和残差逻辑全部验证干净,再动手并行版。先吃到ADMM的"稳",再去追它的"快",这条路最顺。