最近帮学生调一个微网群协调调度项目,模型不复杂,但第一次从集中式转到分布式时,问题一个接一个来。最后用目标级联法(ATC)把问题拆开,才把收敛曲线跑顺。这里整理一份从原理到Matlab实现的完整笔记,目标就是让正在琢磨“微网群多主体分布式优化调度”的同学少走弯路。这个方向不是冷门,做园区多微网、新能源小区、甚至区域综合能源系统的人都会碰到:多个微网各自有独立运营方,又需要在公共联络线上互相支援,谁也不想把内部数据全交出去。ATC正好解决这个矛盾,而且Matlab写起来比想象中简单。
下面我会按“场景拆解 -> 数学原理 -> 代码实现 -> 参数调优 -> 问题排查”的顺序讲,尽量说人话,不堆公式。适合刚接触分布式优化的研究生,也适合从集中式调度转过来的工程师。
1. 项目场景拆解:微网群调度为什么需要“分布式”
1.1 单微网调度和微网群调度的本质差别
单个微网调度时,事情很单纯:一个控制中心能拿到光伏、风电、储能、燃气轮机、负荷的全部数据,然后求一个最小成本或最小碳排放的24小时机组组合。这本质上是一个集中式优化问题,约束、目标都很清楚,直接调求解器就行。
但换成微网群,场景立刻复杂起来。假设园区里有4个微网,每个微网内部有光伏和储能,微网之间通过公共联络线连在一起,还能向大电网买电。表面上看,把4个微网的模型统统拼进一个超大优化问题里,理论上可以求出全局最优解。问题是现实里没人愿意这么干。
为什么?因为“微网群”里的每个微网往往是不同主体的资产,可能是不同企业、不同部门,甚至不同业主。你的光伏出力曲线、储能SOC、负载报价这些数据,属于商业隐私。集中式调度意味着某个中心必须拿到全部数据,还要替大家做统一决策,这在组织上很难推动。更麻烦的是,一旦这个中心出现故障,整个微网群调度就瘫了。
所以分布式优化的本质诉求不是“算得快”,而是“各自算自己的,只交换少量边界信息”。每个微网保留自治权,内部怎么调度自己说了算,只有联络线交换功率这种耦合信息需要跟邻居或上层协调。这样一来隐私保住了,单点故障的风险也降低了,扩展新微网只需加一个子问题,不用重写整个大模型。
1.2 为什么选目标级联法而不是集中式或ADMM
要做分布式优化,主流路线有三条:集中式、ADMM、目标级联法。先说结论:集中式适合做基准对比,ADMM适合两区块问题,而微网群这种“群调度中心-多个成员微网”的层级结构,用目标级联法(Analytical Target Cascading,ATC)最顺手。
集中式的缺点前面提过,这里不展开。ADMM在学界很火,尤其多智能体一致性优化,但它本质是为两区块问题设计的。微网群是典型的多区块:一个中心对N个微网。虽然可以把问题重新包装成ADMM能被的形式,但工程实现时要注意的细节不少,乘子更新和残差平衡都要小心,否则振荡起来调参成本很高。另外,ADMM更像一个扁平的、对等的协调框架,和微网群天然“上有群调度中心、下有各微网”的管理层级不那么贴合。
ATC就很匹配这种层级结构。它最早来自产品设计领域的多学科优化,专门解决“总体部门定目标、多个子系统并行执行并反馈”的问题。ATC的核心思路是把整体目标逐级向下传递,每个子系统先基于上级目标做本地优化,再把本地结果反馈给上级,上级根据反馈修正目标值,如此循环直到上下一致。微网群调度会议上,群调度中心先给每个微网下发电功率交换目标,微网自己调度完把实际功率需求报上来,中心再调整目标,这不就是同一个套路吗?
我把几个方案的适用情况整理成了表:
| 维度 | 集中式 | ATC | ADMM |
|---|---|---|---|
| 协调结构 | 全局单中心 | 上层协调层 + 下层子问题 | 对等多智能体或中心化对偶 |
| 信息交互 | 需要全部数据 | 只交换联络线目标值和反馈值 | 只交换边界变量和乘子 |
| 隐私保护 | 无 | 较好 | 较好 |
| 算法理解难度 | 简单 | 直观 | 中等偏难 |
| 多区块扩展性 | 一般 | 好 | 需要注意构造 |
| 工程案例 | 广泛 | 多学科优化、电力调度 | 统计、信号、电力 |
这个对比不是我随便写的,是实际跑代码之后的体感。ATC在每个微网子问题里只需要加一个二次惩罚项,剩下的事情照旧,非常适合快速出成果。
2. 目标级联法的数学原理,用工程的话讲清楚
2.1 先理解“上级定目标、下级自优化”
ATC的运行逻辑可以拿建筑项目来类比。总包方给每个分包方下达目标:你这周要完成多少工作量,成本控制在多少以内。分包方不关心其他分包方怎么做,只根据自己人、材料、机械的实际情况,把实现这个目标的代价报给总包方。总包方拿到反馈后发现有人报太高、有人报太低,于是调整下一轮目标,重新分下去。这样反复几轮,各分包方的目标趋于一致,总包方也得到了一版大家都可执行的计划。
微网群的每轮迭代就是这个过程。协调层的输入是每个微网的“期望联络线功率曲线”,微网把这个期望值作为外部指令,在内部求解自己的经济调度,然后告诉协调层“我实际最想要的交换功率是多少”。协调层把所有微网反馈综合起来,更新期望值。注意到一个关键点:微网内部数据全程不出域,协调层只看到边界上的功率和它对应的成本影子,这就是ATC的隐私保护机制。
与建筑分包不同的是,微网调度还需要一个“价格信号”来引导不同微网之间互相配合。如果只靠目标值硬压,微网会为了满足目标而牺牲自身经济性,而协调层完全不知道这种牺牲该不该发生。所以ATC引入了拉格朗日乘子,相当于给边界功率贴了一个“影子价格”。这个价格随着迭代自动调整,久而久之各个微网的边际成本会趋于一致,这正是经济调度的最优条件。
2.2 ATC的标准数学模型与代码对照
用数学语言写,微网 i 在第 k 轮要解决的子问题是:
min f_i(x_i) + λ_i^T (x_i^c - t_i) + ρ * || x_i^c - t_i ||_2^2
s.t. x_i ∈ Ω_i
这里的 x_i 是微网 i 的全部决策变量,比如机组出力、储能充放电功率;x_i^c 是其中与外部耦合的边界变量,也就是微网和公共联络线交换的功率曲线;t_i 是协调层本轮下发的期望交换功率;λ_i 是拉格朗日乘子,ρ 是惩罚系数;f_i(x_i) 是微网内部运行成本。Ω_i 表示本地约束集合,包括功率平衡、储能SOC、机组爬坡等。
协调层收到所有微网反馈的 x_i^c* 之后,更新下一轮目标值。理论上的协调层优化问题是:
min Σ_i [ λ_i^T (x_i^c* - t_i) + ρ * || x_i^c* - t_i ||_2^2 ]
但在工程代码里,很少有人直接求解这个上层优化,更常见的是用松弛更新:
t_i^{k+1} = α * t_i^k + (1 - α) * x_i^{c, k}
乘子更新则用:
λ_i^{k+1} = λ_i^k + 2ρ * (x_i^{c, k} - t_i^k)
第一眼看到这些公式可能觉得抽象,但放到代码里很好理解。我下面会给出可直接改的Matlab循环框架。
2.3 乘子更新与收敛判据怎么设
乘子更新为什么重要?因为只更新目标值不做乘子更新,相当于永远在“平均”各方意见,收敛慢而且最终解没有经济学含义。引入 λ 之后,每个微网的子问题里多了一个线性项,相当于给联络线功率一个动态调整的电价。这个电价会随着“上报值偏离目标值”自动升高或降低,逼着各微网在满足自身经济性的同时逐步对齐联络线功率。
收敛判据通常看两个偏差。第一个是耦合量残差,也就是当轮上报值与目标值之间的最大偏差:
gap1 = max | x_i^{c, k} - t_i^k |
第二个是相邻两轮目标值的变化量:
gap2 = max | t_i^k - t_i^{k-1} |
gap1 反映微网实际反馈和协调层目标之间的脱节程度,gap2 反映迭代是否还在“移动”。工程上我一般设 gap1 < 1e-3,gap2 < 1e-3,单位跟着功率走;如果数据是kW,1e-3 已经相当严格,实际中调到 1e-2 也可能完全够用。最大迭代次数我常设 200,如果跑到 200 还没收敛,说明参数有问题,不是迭代次数不够。
惩罚系数 ρ 可以固定,也可以每轮递增。固定值方便调试;递增策略是 ρ_{k+1} = min(ρ_max, β·ρ_k),β 取 1.01~1.05。我个人偏好先固定 ρ 跑通,再看效果决定要不要递增。原因很简单:分布式算法本来变量就多,一上来就引入自适应策略,出了问题很难定位。
3. Matlab代码实现:从零搭起ATC优化框架
3.1 整体代码结构
按我的习惯,一个ATC调度项目至少拆成4个文件:主循环 main_atc.m、微网子问题函数 microgrid_opt.m、场景数据生成 create_scenario.m、结果绘图 plot_results.m。这样分工清楚,后面调参也方便。
main_atc.m 的骨架大概长这样:
%% 初始化 N = 4; % 微网数量 T = 24; % 调度周期,小时 rho = 0.01; % 惩罚系数 lambda = zeros(N, T); % 拉格朗日乘子 target = zeros(N, T); % 协调层下发的交换功率目标 P_ex = zeros(N, T); % 各微网上报的交换功率 maxIter = 200; tol = 1e-3; for iter = 1:maxIter % 各微网并行求解本地优化 for i = 1:N [x, cost] = microgrid_opt(i, target(i, :), lambda(i, :), rho); P_ex(i, :) = x.P_ex; end % 收敛判断:上报值与目标值的最大偏差 gap = max(abs(P_ex - target), [], 'all'); % 保存旧目标,用于乘子更新 target_old = target; % 更新目标值 target = 0.5 * (target_old + P_ex); % 更新拉格朗日乘子 lambda = lambda + 2 * rho * (P_ex - target_old); if gap < tol fprintf('迭代第%d轮收敛\n', iter); break; end end这个框架特别像“先给目标,再听反馈,再调和”的分包循环。微网数量 N 想改多大都行,只要 microgrid_opt 函数能正确读取对应微网的参数,其他代码不用动。
3.2 单个微网内部优化怎么建模
microgrid_opt 是整套代码的灵魂。每个微网虽然参数不同,但模型结构基本一样。我以4个微网为例,里面含有光伏、风机、燃气轮机和储能,变量定义如下:
| 变量 | 含义 | 维度 | 单位 |
|---|---|---|---|
| P_g | 燃气轮机出力 | 1×T | kW |
| P_b | 储能净放电功率 | 1×T | kW |
| P_buy | 从大电网购电功率 | 1×T | kW |
| P_ex | 与微网群其他微网交换功率 | 1×T | kW |
| SOC | 储能荷电状态 | 1×(T+1) | % |
子问题的目标函数由三部分组成:本地供电成本、与大电网的交易成本、ATC惩罚项。本地成本里,燃气轮机常用二次函数 aP_g^2 + bP_g + c,储能用一个线性损耗成本近似,购电成本用分时电价乘以购电量。把这些都加起来:
min sum( a * P_g.^2 + b * P_g + c + c_buy * P_buy + c_bat * abs(P_b) ) + lambda * P_ex' + rho * sum((P_ex - target).^2)
约束部分需要注意几点。功率平衡约束是每个时段都成立的:
P_pv + P_wt + P_g + P_b + P_buy + P_ex = Load
储能约束包括SOC递推和容量上下限:
SOC(t+1) = SOC(t) + η_ch * P_ch(t) - (1/η_dis) * P_dis(t)
不要忘了燃气轮机爬坡约束、联络线容量约束:
- P_ex_max ≤ P_ex(t) ≤ P_ex_max
min - P_ex_max ≤ P_ex(t) ≤ P_ex_max。
严格讲,充电和放电不能同时进行,直观上看最优解也不会同时充放,因为你不会一边高价买电给储能充电,一边让储能放电卖给电网,除非分时电价倒挂。但为了稳妥,我建议把储能建模成一个净功率变量 P_b,正数表示放电,负数表示充电,再加一个约束:
- P_ch_max ≤ P_b(t) ≤ P_dis_max
这样就不需要二进制变量,也能避开同时充放的病态解。如果你的论文需要特别严格的模型,可以用 binvar 引入充放状态,但Matlab纯quadprog里就会麻烦很多,Yalmip会更方便。
3.3 协调层与microgrid_opt函数的关键代码
microgrid_opt 的输入是微网编号、目标值、乘子和惩罚系数,输出是优化结果。我这里用Yalmip写最直观,因为它组装约束和变量时不容易出错:
function [x, fval] = microgrid_opt(i, t, lambda, rho, data) T = data.T; P_pv = data.mg(i).P_pv; P_wt = data.mg(i).P_wt; Load = data.mg(i).Load; P_g = sdpvar(1, T); P_b = sdpvar(1, T); P_buy = sdpvar(1, T); P_ex = sdpvar(1, T); SOC = sdpvar(1, T + 1); % 目标函数 a = data.mg(i).a; b = data.mg(i).b; c = data.mg(i).c; c_buy = data.price; c_bat = data.mg(i).c_bat; Cost = sum(a * P_g.^2 + b * P_g + c + c_buy * P_buy + c_bat * abs(P_b)); ATC_cost = lambda * P_ex' + rho * sum((P_ex - t).^2); % 约束 Constraint = []; for k = 1:T Constraint = Constraint + [P_pv(k) + P_wt(k) + P_g(k) + P_b(k) + P_buy(k) + P_ex(k) == Load(k)]; Constraint = Constraint + [data.mg(i).P_g_min <= P_g(k) <= data.mg(i).P_g_max]; Constraint = Constraint + [-data.mg(i).P_ex_max <= P_ex(k) <= data.mg(i).P_ex_max]; Constraint = Constraint + [0 <= P_buy(k)]; end Constraint = Constraint + [SOC(1) == data.mg(i).SOC_init]; for k = 1:T Constraint = Constraint + [SOC(k+1) == SOC(k) + ...]; Constraint = Constraint + [data.mg(i).SOC_min <= SOC(k+1) <= data.mg(i).SOC_max]; end ops = sdpsettings('solver', 'quadprog', 'verbose', 0); optimize(Constraint, Cost + ATC_cost, ops); x.P_ex = value(P_ex); x.P_g = value(P_g); x.P_b = value(P_b); x.SOC = value(SOC); fval = value(Cost + ATC_cost); end这里要注意:每个微网在子循环里是依次求解的,如果微网数量特别多,比如几十个,可以考虑把 for 循环改成 parfor,但要确认 microgrid_opt 里没有共享变量。实际项目里4到10个微网用普通 for 循环完全够,没必要为了并行去折腾 worker 通信。
3.4 Matlab环境相关的小坑
结合最近几个学生反馈的问题,我补充一点环境相关的经验。Matlab 2023 版本在打开老脚本时容易遇到中文注释乱码,这个跟算法无关,纯粹是文件编码问题。系统默认编码是 GBK,而很多代码文件在保存时是 UTF-8,或者反过来。解决办法也很简单:用 VS Code 或记事本把文件重新保存为带 BOM 的 UTF-8,或者用 Matlab 编辑器里的“另存为”选择 UTF-8,基本能解决。
另一个高频问题是 Yalmip 报“solver not found”。原因通常是 Yalmip 找不到求解器路径,或者系统里压根没装相应的求解器。如果你只想用 Matlab 自带的 quadprog,只需要在 sdpsettings 里明确写成solver', 'quadprog'就行。不要用默认的,因为 Yalmip 默认可能尝试调 Gurobi/Cplex,没装就会报错。
如果不想装Yalmip,直接用 quadprog 构建 H、f、Aeq、beq 矩阵也可以,但储能SOC递推这种动态约束需要手动拼大矩阵,代码维护起来比较痛苦。所以我个人建议:优先装一个求解器,比如quadprog或Gurobi,然后用Yalmip写模型,既保证可读性,也不会因为工具箱版本差异处处踩坑。由于很多同学是教育版Matlab,可能没有全局优化工具箱,装Yalmip再加一个开源求解器(或者直接用自带的quadprog)都是能跑通的路线。
4. 仿真结果分析:收敛曲线与参数调优经验
4.1 结果应该长什么样
代码跑通后,第一件事不是看功率曲线,而是看收敛曲线。收敛曲线横坐标是迭代次数,纵坐标是 gap 值,也就是每轮上报值跟目标值的最大偏差。正常的ATC收敛曲线应该是前期快速下降,中后期平缓趋零。如果gap是一会儿大一会儿小的锯齿状,说明参数没调好;如果gap一直在一个水平线上不动,说明惩罚系数太小或者目标更新策略太保守。
具体到功率结果,有个判断对错的好方法:把4个微网的P_ex曲线横着加起来,再加上从大电网购入的功率,应该等于微网群与大电网的交换总功率。如果群内有微网的P_ex为正、有微网的P_ex为负,说明确实存在微网之间的互相支援,这是微网群调度的意义所在。
我通常还会跑一个集中式优化作为基准。集中式就是把4个微网的模型合并成一个优化问题,目标函数和约束完全相同,求解一次得到全局最优解。分布式ATC运行结束后,把总成本和集中式总成本对比。如果分布式结果和集中式结果偏差在1%以内,说明算法调得不错;如果偏差到5%以上,就要检查是不是收敛精度不够,或者联络线耦合变量设置错了。
4.2 惩罚系数ρ的调参心得
ρ是ATC里最敏感的参数,我的调参顺序永远是先固定ρ跑通,再考虑递增。
ρ太小的时候,子问题里惩罚项的权重很低,各微网几乎无视协调层下发的目标,反馈回来的交换功率跟目标差很远,gap迟迟降不下去,收敛极慢。ρ太大的时候,惩罚项几乎盖过了微网本地运行成本,微网会为了硬凑目标值而牺牲经济性,结果就是迭代过程中目标值来回大幅跳变,甚至不收敛。
我的经验是,如果数据单位是kW和元,ρ从0.01开始试。每个微网的目标函数是二次成本,量级可能在几十到上百元,联络线功率可能在几十到几百kW。ρ取0.01时,二次惩罚项在交换功率偏差50kW时大概是25元,这个量级刚好能影响但不淹没本地成本。如果发现gap下降太慢,把ρ乘以5,比如从0.01调到0.05,再跑一遍看趋势。这样试三轮基本能确定合适的量级。
如果你希望更精细一点,可以采用递增策略。每迭代一轮让ρ乘1.02或1.03,但设一个上限,防止后面爆炸。递增策略的好处是前期让各微网自由探索,后期让边界功率快速收敛。缺点是参数多了,出了问题不好定位。所以我的默认选择还是固定ρ,只有在固定ρ效果不理想时才考虑递增。
4.3 初始目标和乘子的设置技巧
初始目标不能随便设成零。如果一开始就告诉所有微网“你们跟外部的交换功率目标是0”,微网只能靠内部机组和购电竞价来平衡,第一轮反馈上来的P_ex可能和0差得很远,后面迭代要花很多轮才能把目标值拉回来。更聪明的做法是先用单独优化每个微网,不管ATC耦合,求出各自最优的交换功率,作为协调层的初始目标值。这样第一轮gap就不大,相当于热启动。
乘子初值一般设成0就可以。如果觉得初始迭代太慢,可以用上一轮运行结果中的乘子作为下一轮初值,这种热启动在重复运行长时间尺度调度时尤其有效。比如做全年365天调度,每天都是24小时问题,直接沿用前一天收敛后的乘子作为初始值,往往能把迭代次数从几十次压缩到十几次。
还有一个容易被忽略的细节:不同微网的规模差异很大时,需要做归一化处理。比如微网1最大负荷500kW,微网2最大负荷只有80kW,两个微网的P_ex量级差6倍。如果统一用同一个ρ和同一个收敛阈值,小微网可能早早收敛,但小微网的反馈在大微网眼里只是噪声。这时候把功率统一换算成标幺值,或者每个微网单独设置阈值,都比强行改ρ要有效。
5. 常见问题与排查技巧实录
5.1 不收敛、振荡、求解器报错
先说不收敛。我遇到的不收敛案例里,一半以上不是ATC算法的问题,而是某个微网子问题在给定目标下压根无解。怎么验证?单独掉出那个微网,跑microgrid_opt函数,看是否报infeasible。如果确实无解,就要检查联络线容量约束、储能SOC上下限、以及负荷功率平衡是否一致。尤其是储能SOC初值和光伏出力曲线,如果某一个时段负荷特别高而机组容量和联络线容量都不够,子问题无解,协调层目标再怎么发也没用。
振荡的问题更常见。典型场景是迭代到十几次时gap不降反升,画出来像一条锯齿状曲线。排除ρ之后,最常见的原因是目标值更新太激进。如果你使用的更新公式是 t = P_ex,也就是完全用微网反馈替换目标,那么下一轮微网又要调整自己的解去追这个新目标,很容易来回摆动。改成 t = 0.5t + 0.5P_ex 这种加权平均后,相当于给目标更新加了“惯性”,多数振荡能压下去。
求解器报错就五花八门了。Yalmip报维度不匹配时,检查每个变量的维数是不是1×T,尤其是SOC是1×(T+1),差分约束里别写错索引。quadprog报H矩阵不是半正定,通常是把二次项写成了向量乘向量的形式,要用二次型或者Yalmip自动处理。还有一种是迭代过程里出现NaN,很可能某轮子问题无解或惩罚项太大造成数值溢出,优先检查ρ是否过大。
5.2 问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| gap一直不降 | ρ过小,惩罚项影响太弱 | 增大ρ,比如从0.01调到0.05 |
| 迭代十几代后发散 | ρ过大或乘子更新步长过大 | 减小ρ,或把乘子更新系数从2改成0.5 |
| 曲线锯齿振荡 | 目标值更新太激进 | 改成加权平均 t = 0.5t + 0.5P_ex |
| 某个微网报infeasible | 本地约束过强或负荷不平衡 | 单独验证该微网无ATC项时是否有解 |
| 结果与集中式差距大 | 收敛精度不够或初始目标不合理 | 提高迭代上限,或热启动初始目标 |
| Yalmip报solver not found | 求解器路径或名称配置不对 | 指定 solve('solver','quadprog') |
| 中文注释乱码 | 文件编码不统一 | 另存为UTF-8或改系统编辑器编码 |
5.3 一条独家调试经验
最后分享一个我自己的调试习惯:先搭一个2微网的小案例,跑通后再扩展到N微网。2微网时物理直觉很清晰,微网1多送的功率一定等于微网2少买的功率,任何一个gap变化都能从联络线功率曲线上直接看出来。很多在4微网情况下说不清的问题,缩到2微网就非常明显。我在实际项目里几乎都是用2微网把ATC从0调到收敛,再改参数表中的N为4或10,后面的问题基本都是参数问题而不是算法问题。
另外提醒一下,不要指望分布式结果比集中式更优。分布式优化的意义是隐私保护和自治权,代价是理论上可能得到次优解。如果某个案例分布式结果明显差于集中式,先检查收敛精度是不是没到位,而不是怀疑算法选错了。
我在实际调试中最深的体会是:ATC这类方法,算法本身其实不复杂,真正花时间的是子问题建模和参数标定。代码写错了,收敛曲线会直接告诉你;模型参数不合理,迭代调一年也没用。如果你也正准备做微网群分布式优化,建议先把单个微网的物理约束和成本算清楚,再上ATC耦合层。从两微网开始,跑通一次,你就能摸到这套方法的脾气了。