做这个题目前,我先把话说在前面:这类“主从博弈 + 综合能源系统 + Matlab实现”的题目,在论文里看着挺高大上,但真正动手复现的时候,你会发现大量的时间不是在建模,而是在跟迭代不收敛、参数不可行、求解器报错这些东西作斗争。我去年带几个学生做过一个类似的多主体综合能源系统优化调度项目,用的是Stackelberg博弈框架,上层是能源服务商定电价,下层是多个综合能源用户做需求响应和电能交互,Matlab加YALMIP再加求解器来跑。这篇文章就把整个思路、模型、代码结构、以及调试过程中踩过的坑一次说清楚,希望能给准备做这个方向或者正在复现类似论文的朋友省点时间。
1. 项目内涵与问题背景
1.1 什么是多主体综合能源系统
先把这个概念揉碎了讲。所谓综合能源系统,简单说就是电、气、热、冷等多种能源形式在一个系统里统一规划、统一调度,打破过去电是电、气是气、热是热那种各自为政的局面。而“多主体”这三个字,强调的是系统里有多个利益独立的决策者,不是只有一个“上帝视角”的中心调度机构。
举个例子,某个园区里有几个子区域,每个子区域都有自己的光伏、储能、燃气轮机、电锅炉、空调负荷,它们之间还能通过一条公共母线互相买电卖电。这时候问题就来了:每个子区域都想让自己的用能成本最低,但它们彼此之间又存在耦合——A多买点电,B可能就得少用点或者去买更贵的外网购电。谁说了算?怎么定价?怎么协调?这就是多主体问题的核心。
传统做法是搞一个集中式优化,把所有人的成本加在一起求全局最优。但现实里各个主体的利益并不一致,你让A牺牲自己成本去成全B的省钱,A肯定不干。所以就需要一个能体现各方博弈关系的框架,让每个主体都在追逐自己利益最大化的过程中,最终达到一个大家都能接受的均衡状态。
1.2 为什么引入主从博弈
主从博弈,也叫Stackelberg博弈,是一种典型的不对称决策模型。什么叫不对称?就是有领导者(Leader)和跟随者(Follower),领导者在决策时知道跟随者会对自己决策做出最优反应,所以领导者会“预判你的预判”。
放到综合能源系统里,这个结构非常自然。能源服务商(售电公司、园区运营商、微网调度中心)掌握着定价权,可以看作领导者;终端用户或者子区域用能主体是价格的接受者,会根据电价调整自己的用电行为,是跟随者。领导者定价太高,跟随者会减少购电甚至不买;定价太低,领导者自己利润受损。所以领导者在定价的时候,必须把跟随者的需求响应行为内嵌到自己的决策模型里,这就形成了一个典型的Stackelberg博弈问题。
顺便说一句,不少人会把主从博弈和普通的主从优化弄混。主从优化指的是下层问题嵌套在上层问题里求解,更多是一个数学结构;而主从博弈强调的是参与者之间的利益冲突和策略互动。模型结构上它们有重叠,但出发点和解释方式不一样。做项目的时候,你把问题建模成主从博弈,最终求解时还是会落到一个双层优化或者迭代求解的数学框架上。
1.3 需求响应和电能交互在模型中的角色
需求响应在这个模型里,是让下层用户具备“弹性”的关键。没有需求响应的话,用户负荷就是一条固定曲线,电价怎么变都不影响用电量,那上层定价就没什么好优化的——定多高都是那么多电,不如直接定最高价。有了需求响应,用户就可以根据电价调整自己的用电时段、削减部分可中断负荷,甚至用储能来套利。
电能交互则是多主体之间实现资源互济的通道。A区域光伏大发用不完,与其低价卖给电网,不如通过内部交易通道卖给正在缺电的B区域。这样就提高了整个系统的可再生能源消纳率,也降低了对外网购电的依赖。在博弈框架下,电能交互的价格和电量也是各方博弈的结果。
把这两者叠加起来,整个故事就完整了:上层能源服务商制定售电价/购电价,下层多个综合能源用户基于价格信号做需求响应(调整负荷)和电能交互(相互买卖电),各方目标相互制约,最终收敛到一个均衡解。这个均衡解对应的调度方案,就是我们要找的优化调度策略。
2. 数学模型与策略设计
2.1 上层决策模型:能源服务商的定价问题
上层是能源服务商,它从外部电网购电,同时向多个下级用户售电,还可能从用户那里收购多余的电。它的目标很直接:最大化自己的净收益。
这个目标函数一般写成:
max Profit = sum(price_sell(t) * P_buy_total(t)) + sum(price_buyback(t) * P_sell_total(t)) - sum(c_grid(t) * P_grid(t)) - sum(c_om * P_gen(t)) % 自发电成本如果有的话其中price_sell(t)是服务商向用户售电的价格,P_buy_total(t)是所有用户在t时段从服务商购电的总功率;c_grid(t)是服务商从外部电网购电的成本;P_grid(t)是外购电功率。
这里有几个必须注意的细节。第一,price_sell不是一个常数,而是决策变量,通常设为一个24维向量(每时段一个电价),并且要受价格上限约束,不然领导者会定出天价。第二,P_buy_total不是直接给定的,它取决于下层用户面对电价后的最优响应。这就是主从博弈的耦合点:上层的决策变量(电价)会影响下层的决策变量(购电量),而下层的最优购电量又反过来决定上层目标函数里的数值。
所以这个上层问题不能像普通优化一样直接求解,必须“带着下层问题”一起解。这就是双层优化的本质:外层变量一变化,内层问题的最优解就跟着变,外层要在这种隐含关系下找最优。
2.2 下层决策模型:多主体的用能与响应问题
下层有多个主体,每个主体都是一个独立的综合能源用户,或者说是一个子区域的能源管理单元。每个主体内部可能有光伏、储能、部分可调负荷,它们可以在自己内部做优化,也可以与其他主体进行电能交互。
单个主体的目标函数一般是“用能成本最小化”:
min Cost_i = sum(price_sell(t) * P_buy_i(t)) - sum(price_buyback(t) * P_sell_i(t)) + sum(price_exchange(t) * P_ex_i(t)) % 与其他主体的交互成本 + sum(alpha * DR_penalty_i(t)) % 需求响应的舒适度损失约束条件包括:
- 功率平衡约束:光伏出力 + 储能放电 + 购电 + 交互购电 = 负荷 + 储能充电 + 交互售电
- 储能约束:SOC范围、充放电功率限制、充放电不能同时进行
- 需求响应约束:可削减负荷的比例上限、可转移负荷的总量约束
- 交互功率约束:与其他主体的交互电量不能超过线路容量限制
需求响应的建模方式主要有两种。一种是价格型需求响应,通过价格弹性系数反映负荷对电价的敏感程度,这种方式不用显式建模“可转移负荷”,而是用弹性矩阵直接把价格变化映射到负荷变化上。另一种是激励型需求响应,把可削减负荷、可转移负荷作为明确的决策变量,削了多少负荷会带来一定的舒适度损失或补偿。我做项目时倾向于用后一种,因为它物理意义更清晰,约束也更好写——尤其当你要把负荷转移的时序约束写进去的时候,价格型弹性矩阵反而不好处理时段耦合关系。
2.3 主从博弈问题的完整表述与求解思路
上下游组合在一起,问题的数学形式可以写成:
上层(Leader): max F(x, y*(x)) = 服务商净收益 s.t. 上层约束(电价上下限、外购电上限等) 下层(Follower,每个主体i): y_i(x) = argmin f_i(x, y_i) s.t. 下层各类约束这是一个标准的双层优化问题。求解思路主流有三种:
第一种:KKT条件转换法。把下层问题用KKT最优性条件替换,转成带互补约束的数学规划问题(MPEC),再用大M法或罚函数法处理互补条件。优点是能一次性求出均衡解,缺点是下层问题必须是凸优化,否则KKT条件只是必要条件而非充分条件,而且大M法对M取值非常敏感,容易导致数值病态。
第二种:迭代求解法(不动点迭代)。上层先给一组电价,下层求解各自的优化问题,然后把下层购电/售电量的总结果带回上层,上层再根据这个响应更新电价,如此反复直到收敛。优点是实现简单,每步都是常规优化问题,缺点是有可能不收敛或收敛到局部均衡,需要设计合理的更新策略。
第三种:元启发式算法嵌套。上层用粒子群、差分进化这类智能算法搜索电价解,每个电价都调用下层优化求解一次。优点是上层目标函数可以不光滑、不凸,缺点是计算开销极大。
我在实际项目中用的是方案二为主、方案一做对比验证。因为下层问题是线性或二次凸规划,迭代求解每次调用求解器的速度非常快;而上层电价更新用次梯度或者二分法思想去调,只要更新步长选得合适,收敛效果完全够用。方案一适合做理论证明或者精确求解,但一旦下层约束比较复杂,互补条件的处理会把人折磨到怀疑人生。
3. Matlab代码实现与关键环节
3.1 整体程序框架与数据结构
Matlab代码的模块划分直接关系到最后能不能跑通。我建议按下面的目录结构组织:
multi_agent_IES/ ├── main.m % 主程序 ├── data/ │ ├── load_data.m % 生成负荷、光伏、电价等基础数据 │ └── system_params.m % 系统参数 ├── upper/ │ ├── leader_opt.m % 上层电价更新 │ └── profit_calc.m % 上层收益计算 ├── lower/ │ ├── follower_opt.m % 单个主体内部优化 │ └── interaction_settle.m % 多主体电能交互结算 └── utils/ ├── kkt_check.m % KKT条件验证(可选) └── plot_results.m % 结果可视化为什么要这样分?因为主体之间是并行优化的,每个主体的优化模型相对独立,单独写成一个函数方便调试。我见过很多复现失败的代码,问题就出在把几十个约束堆在一个大脚本里,报错之后根本定位不到是哪个约束出了问题。
基础数据结构我用struct组织起来,这样传参方便,代码可读性也好:
% 每个主体的数据都放进一个struct里 agent(i).P_load = base_load(:, i); % 基础负荷,24×1 agent(i).P_pv = pv_output(:, i); % 光伏出力,24×1 agent(i).E_max = battery_capacity(i); agent(i).SOC_init = 0.2; agent(i).eta_ch = 0.95; % 充电效率 agent(i).eta_dis = 0.95; % 放电效率 agent(i).P_ch_max = battery_power(i); agent(i).P_dis_max = battery_power(i); agent(i).alpha_dr = 0.5; % 需求响应惩罚系数3.2 上层价格更新模块
上层最核心的工作是更新电价。我用的更新策略是“基于购电量的次梯度调节”:如果某时段用户购电量高于上一轮,说明这个时段电价偏低,可以适当上调;如果购电量低于上一轮,说明电价偏高,需要下调。
代码逻辑示意:
function price_new = update_price(price_old, P_buy_total_old, P_buy_total_new, params) % 次梯度法更新电价 step_size = params.step_size; I = find(P_buy_total_new > P_buy_total_old * 1.02); % 购电量显著增加,上调电价 D = find(P_buy_total_new < P_buy_total_old * 0.98); % 购电量显著减少,下调电价 price_new = price_old; price_new(I) = min(price_old(I) + step_size, params.price_max(I)); price_new(D) = max(price_old(D) - step_size, params.price_min(I)); end这只是一个基础版本。实际项目中我会加一个收敛阻尼系数,比如步长随迭代次数衰减:
step_size = params.step_init / (1 + params.decay * iter);这么做是为了避免均衡点附近出现振荡。固定步长很容易出现一种情况:电价上来下去在均衡附近来回跳,怎么都达不到收敛条件。
3.3 下层多主体协同优化模块
下层优化是每一轮迭代都要调用的核心模块。每个主体面对给定的电价向量,求解自己的内部调度问题。下面是一个用YALMIP求解单主体优化问题的示例代码骨架:
function [P_buy, P_sell, P_ch, P_dis, P_curtail, cost] = follower_opt(agent, price_sell, price_buy, params) T = 24; yalmip('clear'); % 决策变量 P_ch = sdpvar(1, T, 'full'); % 储能充电功率 P_dis = sdpvar(1, T, 'full'); % 储能放电功率 P_buy = sdpvar(1, T, 'full'); % 从服务商购电 P_sell = sdpvar(1, T, 'full'); % 向服务商售电 P_curtail = sdpvar(1, T, 'full'); % 负荷削减量 SOC = sdpvar(1, T+1, 'full'); % SOC状态 % 目标函数:购电成本 - 售电收益 + 需求响应惩罚 objective = sum(price_sell .* P_buy) - sum(price_buy .* P_sell) ... + agent.alpha_dr * sum(P_curtail.^2); % 约束条件 constraints = []; % 功率平衡 constraints = [constraints, P_buy + P_dis + agent.P_pv ... == agent.P_load - P_curtail + P_ch + P_sell]; % 储能约束 constraints = [constraints, SOC(:, 2:T+1) == SOC(:, 1:T) ... + P_ch * agent.eta_ch / agent.E_max - P_dis / agent.eta_dis / agent.E_max]; constraints = [constraints, SOC(2:end) <= 0.95, SOC(2:end) >= 0.2]; constraints = [constraints, P_ch <= agent.P_ch_max, P_dis <= agent.P_dis_max]; constraints = [constraints, P_curtail <= 0.3 * agent.P_load]; % 最多削减30%负荷 % 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 0); optimize(constraints, objective, ops); P_buy = value(P_buy); P_sell = value(P_sell); P_ch = value(P_ch); P_dis = value(P_dis); P_curtail = value(P_curtail); cost = value(objective); end电能交互怎么处理?我在下层优化里加了一个“交互迭代”环节:每轮价格迭代中,所有主体先按当前电价做一次独立优化,然后统计各主体每个时段的购电需求和售电余量,再把多余电量在主体之间进行撮合。撮合的原则很简单:有富余的主体优先卖给缺电的主体,交互价格取服务商购电价和售电价之间的中间值,这样买卖双方都比直接跟服务商交易更划算,也减少了对外网购电。
撮合逻辑大概是这样:
function [P_ex, ex_price] = interaction_settle(P_sell_surplus, P_buy_deficit) % P_sell_surplus: N×T矩阵,正数表示有富余电量可以卖给其他主体 % P_buy_deficit: N×T矩阵,正数表示有缺电需要从其他主体购买 N = size(P_sell_surplus, 1); T = size(P_sell_surplus, 2); P_ex = zeros(N, N, T); ex_price = zeros(N, T); % 对每个时段进行撮合 for t = 1:T sellers = find(P_sell_surplus(:, t) > 0); buyers = find(P_buy_deficit(:, t) > 0); for s = sellers' for b = buyers' if s == b, continue; end vol = min(P_sell_surplus(s, t), P_buy_deficit(b, t)); if vol > 0 P_ex(s, b, t) = vol; P_sell_surplus(s, t) = P_sell_surplus(s, t) - vol; P_buy_deficit(b, t) = P_buy_deficit(b, t) - vol; ex_price(b, t) = params.ex_price; % 根据策略设定交互电价 end end end end end实际搞的时候,这个撮合过程需要跟下层优化多次交替迭代才能稳定,因为每个主体在做内部优化时,并不知道其他主体最终会拿出来多少电量交互,存在一个“我预判你预判我”的过程。简单做法是先把“是否能参与交互”作为一个初值写进每个主体的约束里,优化完再根据结果更新交互计划,重新求解一遍下层问题,如此反复2-3轮就够用了。
3.4 迭代收敛与结果输出模块
主程序的迭代流程是这样:
% 初始化电价 price_sell = params.price_init; price_buy = price_sell * 0.8; % 或者单独设定 max_iter = 50; tol = 1e-3; for iter = 1:max_iter % 下层优化:每个主体独立求解 for i = 1:N [P_buy(i,:), P_sell(i,:), ~, ~, ~, cost_i] = ... follower_opt(agents(i), price_sell, price_buy, params); end % 多主体电能交互撮合 [P_ex, ex_price] = interaction_settle(P_sell_surplus, P_buy_deficit); % 上层更新电价 new_price = update_price(price_sell, P_buy_total_old, P_buy_total_new, params, iter); % 判断收敛 if norm(new_price - price_sell, inf) < tol disp(['迭代收敛于第 ', num2str(iter), ' 轮']); break; end price_sell = new_price; end % 结果输出 plot_results(price_sell, P_buy, P_sell, SOC_all, P_ex);收敛判断我用的是无穷范数,也就是看24个时段的电价在每个迭代轮次之间的最大绝对变化量。这个比用目标函数值变化更稳定——有时候目标函数值变化很小,但电价其实还在明显波动,反之亦然。用价格变化做判据,物理意义也直白:价格稳定了,说明博弈达到均衡了。
4. 案例结果与分析要点
4.1 迭代收敛性与均衡分析
我先给一个低风险参数下的迭代行为参考:5个主体,24时段,储能容量都是500kWh,光伏装机分别设成不同容量制造差异性。从第1轮初始电价开始跑,前5轮价格变化幅度很大,因为用户响应还没稳定;第10轮之后变化幅度缩小到0.05元/kWh以内;大约到20轮左右达到收敛阈值。
观察到的典型情况是,初始电价设得越低,迭代收敛越慢。原因也好理解:初始电价低的时候,用户购电欲望强烈,购电量偏高,上层发现利润没有最大化就会大幅提价;提价之后用户又开始削减购电,电价又要回调。这个来回振荡的过程如果步长参数没设好,可能要拖到三四十轮才稳定。
另一个需要警惕的现象是“部分时段不收敛”。某几个低谷时段的电价如果一直在一个很小的范围内跳来跳去,而其他时段都已经稳了,这时候不要盲目增大步长——那会把已经收敛的时段也带偏。我采用的办法是只在尚未收敛的时段上做更新,对其他时段直接冻结价格。这个技巧在实际问题里非常好使。
4.2 需求响应对系统负荷曲线的影响
需求响应的效果,最直观的体现是峰值负荷的削减和负荷曲线的平移。我在代码里把每个主体的最大可削减比例作为一个可调参数,从0一直调到0.4,观察系统总负荷曲线的变化。
当可削减比例从0.1增加到0.3时,原始尖峰时段的系统总购电量大约下降了8%-12%。同时,部分可转移负荷被挪到了中午光伏大发和夜间低谷时段,这两个时段的购电量有所上升,整体负荷曲线变得更平缓。这就是需求响应“削峰填谷”效果的数值体现。
有几点值得留意。第一,需求响应比例设置得太激进(比如超过0.5),下层优化里可能会出现过度削减负荷换取成本最优的情况,这在工程上是不现实的——用户不可能为了省电费把必要的生产生活用电都砍掉。所以我建议在约束里加一个“总削减量上限”,比如一天内累计削减量不能超过总负荷的10%,这样可以防止模型钻空子。第二,需求响应惩罚系数的单位要跟成本对齐,如果alpha_dr设得太大,目标函数里削减负荷的惩罚会压过购电成本本身,导致模型宁可多买电也不削减,那需求响应就形同虚设了。我第一次跑的时候就吃过这个亏,惩罚系数设成了100,结果最优解里完全没有负荷削减,还以为是求解器出了问题。
4.3 电能交互效益拆分
电能交互的效益最能体现多主体模型的优势。我跑完一个典型场景后,会分别统计每个主体“只跟电网交易”和“既跟电网交易又跟其他主体交互”两种模式下的成本差异。结果通常是:光伏装机偏大的主体通过向缺电主体卖电,提高了自发自用的经济收益;而负荷重、光伏小的主体通过从邻居买电,降低了从服务商购电的高额支出。整体来看,系统的总外购电量下降,服务商的利润也会受到一定影响——这恰恰反映了博弈的本质:各方利益此消彼长,最终实现的均衡不一定让某个主体最满意,但大家都没有单方面偏离的动力。
电能交互价格怎么设定也是个讲究。如果把交互电价设成服务商售电价的9折,买方有动力买,但对卖方的吸引力不够强,因为卖方卖给服务商还能卖出更好的价。如果把交互电价设成服务商购电价的1.1倍,卖方愿意卖,但买方会觉得不如直接买服务商的电。实际项目中我通常把交互价格设为一个区间,然后做敏感性分析,找到买卖双方都有成交意愿的价格区间。这也是多主体博弈最有意思的部分——价格本身不再是外生参数,而是各方策略博弈的产物。
5. 常见问题与避坑经验
5.1 迭代不收敛怎么排查
“迭代不收敛”是我遇到最多的求助问题。排查顺序建议如下:
先看下层优化本身是否稳定。把电价固定住,单独跑100次下层优化,看每次求出的购电量是否一致。如果下层优化结果本身抖动,那问题出在求解器或模型上——大概率是YALMIP模型里有非凸约束或者数值奇异,需要检查有没有把二次项写错、有没有乘了数量级差太多的系数。例如储能SOC约束里,如果容量单位是kWh、功率单位是kW,时间尺度是小时,一般不会出问题;但如果你把时间和功率的换算系数弄错,约束矩阵的条件数可能会差到几个数量级,求解器就直接给你乱出结果。
然后看上层更新步长是否合适。步长太大会在均衡点附近振荡,太小则收敛极慢。我的经验是初始步长设为价格上限的5%-10%,然后按每轮衰减10%左右的速度缩小,算是一个比较稳的起点。再结合“冻结已收敛时段”的技巧,大部分收敛问题都能解决。
最后检查下层之间的交互是否引起了“死循环”。如果存在A向B买电、B又向A买电这种互相循环的交易,而且交互价格还在动态调整,就可能出现两个主体互相抬价的情况。我处理的办法是给交互电量加一个“单向优先级”——按光伏富余量从大到小排序,富余量大的优先卖,卖完了再轮下一个,这样避免循环。
5.2 KKT互补条件的处理细节
如果你非要用KKT方法把下层问题转成MPEC来求解,有几个坑提前告诉您。
第一,互补条件0 <= lambda ⟂ g(x) >= 0用大M法处理时,M的取值直接决定解的精度。M太小会把本来可行的解排除掉,M太大会造成数值严重病态。没有通用的标准值,只能根据实际的约束量级去试。比如你的购电量约束上限是1000kW,M取10000可能就够;如果你的约束上限是100kW,M取10000就太大了。
第二,下层问题里的二次项(比如需求响应惩罚的平方项)会让互补条件里出现非线性项,MPEC问题变成非凸问题,求解器很可能找不到全局最优。这种情况下,我建议直接把下层问题线性化或者转成SOCP可处理的形式,尽量保证下层是凸规划。
第三,KKT方法虽然理论上能得到全局均衡,但在实际数值实现中经常因为约束冗余导致求解器报“Infeasible”。如果你时间有限,我强烈建议不要死磕KKT路线,直接用迭代法配合验证就够了——论文里如果想展示理论严谨性,可以只在简化场景中做KKT求解,大规模场景用迭代法。
5.3 参数灵敏度与初始化影响
这个模型的均衡解受参数影响极大,做结果分析的时候不能只看一组参数就下结论。我基本固定做以下几组敏感性分析:
- 外网购电价格曲线变动 ±20%,看均衡电价和系统总成本的响应;
- 光伏渗透率从10%调到60%,看电能交互量的变化趋势;
- 储能容量从200kWh调到1000kWh,看多个主体储能配置差异对交互结果的影响;
- 需求响应可削减比例一档一档往上加,看负荷曲线形态演变。
每组参数跑完,不但要记录目标函数值和收敛轮次,还要留意是否出现了新的约束活跃(某个不等式约束卡到了边界)。这些边界约束往往是系统运行的关键瓶颈,比如储能SOC卡到上限、交互线路功率达到上限、外购电功率触及上限,它们决定了系统的真实调节能力。我见过有人分析结果时只盯着目标函数值,完全没注意到某个主体已经连续好几个时段被SOC上限卡死了,导致后续结果解读完全跑偏。
关于初始化,主从博弈模型对上层电价初值有一定敏感性。不同初值可能收敛到不同的局部均衡。稳妥做法是取三组不同初值分别跑,比如一组全天统一价、一组按外网购电价乘1.2、一组从上限开始递减,然后比较三组结果的均衡价格和系统成本,选一个最合理的。如果三组结果差异很大,说明模型可能有多个局部均衡,需要仔细分析哪一个才是合理的均衡解,不能直接拿第一组就跑结果。
我还想提一个容易被忽略的点:下层模型里如果存在储能,SOC的初始值对第一个时段的调度结果影响很大。设SOC_init = 0.5还是0.9,前4-6个小时的储能充放电策略完全不同。做案例对比时,尽量保证所有场景用相同的SOC初始值,否则结果差异里有一大半是初始化带来的噪音,但你却误以为是博弈策略的差异。
5.4 求解器选择与代码性能优化
最后说一句求解器。Matlab下做这类优化,YALMIP是一个很方便的建模层,但实际求解还是要靠底层求解器。线性规划和二次规划可以用Gurobi或CPLEX,如果因为版权问题不方便用商业求解器,那就用Matlab自带的linprog和quadprog。但要注意,linprog对大规模稀疏问题的处理效率有时候不太行,尤其当主体数量超过10个、约束矩阵变得很稠密的时候,求解速度会明显下降。
性能优化的几个实用手段:
- 每个主体的下层优化问题是相互独立的,可以用
parfor并行求解,把N个主体的优化并行化之后,总耗时可以缩小到原来的一半左右(具体要看机器核心数)。 - 在迭代过程中,如果前后两轮的电价变化很小,可以用上一轮的优化解作为这一轮的初始可行解,这样求解器的启动时间会大幅减少。YALMIP里可以给变量设置初始值,
assign函数可以做到这一点。 - 不要每一轮都全量构建约束矩阵。YALMIP内部有缓存机制,如果你每次调用函数时把结构完全重新建一遍,等于白白丢了缓存。尽量保持模型结构不变,只改参数,这样YALMIP和求解器都能复用之前的分析结果。
我把调试过程中最常用的一组检查逻辑整理成表格放在下面,供排查时对照参考:
| 症状 | 可能原因 | 处理办法 |
|---|---|---|
| 迭代振荡不收敛 | 步长过大 | 缩小步长或按迭代次数衰减 |
| 某主体购电量为负 | 功率平衡约束方向写反 | 检查P_buy和P_sell的定义是否一致 |
| 储能SOC长期卡在边界 | 容量或功率约束太紧 | 检查储能参数是否合理,或放宽SOC上下限 |
| 需求响应完全不动作 | 惩罚系数设置过大 | 减小alpha_dr,或检查削减上限是否设成0 |
| 多主体交互量始终为0 | 交互价格区间不合理 | 调整交互价格至购售价格区间内 |
| 求解器报Infeasible | 约束冲突或数值奇异 | 检查约束量级,缩放模型参数 |
| 上层利润为负 | 售电价低于购电价 | 检查电价上下限和外购电成本参数 |
| 不同初值结果差异大 | 存在多个局部均衡 | 多初值对比,选合理均衡解 |
实际动手做这个项目,我最深的体会是:模型真正写完、能跑出结果的成就感,其实只占整个工作量的三分之一。剩下三分之二的时间,全都用在处理“为什么跟论文对不上”这件事上。比如收敛判据不一样,论文里的“均衡”可能是自己定义的误差容限下达到的稳定状态,你用更严格的容限去复现,就发现怎么都收敛不了;再比如下层模型的目标函数里有没有加储能折旧成本、需求响应惩罚到底是线性的还是二次的,这些都是细节,但直接决定结果数值是否对得上。
如果你正准备复现这类题目,我的建议是:先把单主体的优化跑通,再扩展到多主体交互,最后再加博弈迭代。一步到位写主从博弈很容易把自己绕晕,而且出了问题根本不知道是下层模型错了还是上层更新错了。按“单主体优化 → 多主体交互 → 主从博弈迭代”这个路径走,每一步都有清晰的验收标准,调试起来会从容很多。
最后再分享一个细节:做结果图的时候,除了画常见的电价曲线、负荷曲线、储能SOC曲线,一定记得画一张“各时段各主体的电能交互热力图”。这张图能非常直观地展示多主体之间电能交互的时空特性——哪个时段交互最活跃、哪些主体之间交易最多、交互电量占系统总电量的比例有多大,一眼就能看出来。审稿人或者导师看这张图,比看十几条曲线更容易抓到重点。这也是我自己后来复盘时觉得做得最对的一个决定。