☰
MATLAB复现多能源微网双层模型与滚动优化调度:从建模到求解器实战
2026/9/28 23:26:39 网站建设 项目流程

多能源微网调度这个方向,论文里“双层模型+滚动优化”几乎成了标配,但真正动手用MATLAB复现一遍,才发现问题全藏在细节里:模型怎么分层、日前计划和日内滚动怎么衔接、YALMIP里混合整数变量怎么处理、一天96个时段滚动循环怎么稳定跑完不报错。这篇文章我把整个复现过程完整捋一遍,从模型拆解到代码实现,再到我实际踩过的求解器和收敛性坑,全部写出来。适合正在做微网优化调度、想复现论文代码、或者准备用MATLAB做能源系统仿真的朋友,看完可以直接拿这套框架去改自己的算例。

1. 先想明白一件事:这套模型到底在优化什么

1.1 多能源微网调度的现实痛点

多能源微网和传统微网最大的区别在于能源形式的耦合。系统里通常同时存在电、热、气三种能量流:燃气轮机发电的同时产生余热,余热可以供给热负荷或存入热储能,这就是热电联产(CHP);电锅炉和空气源热泵把电能转化成热能;更前沿一点还有电转气(P2G),把富余的风电转化为氢气或天然气。能量耦合让系统整体效率提升了,但调度难度也上来了。

难点在于两个层面。第一个层面是时间尺度差异:电价和天然气价格的波动周期是小时级的,风光的随机性却可能在十几分钟内剧烈变化,储能的SOC状态又跨小时延续。把所有这些放进同一个优化模型,不是不能做,但模型会非常臃肿,而且一次求解得到的“24小时最优计划”在实际运行中很快就会被预测误差打乱。第二个层面是决策类型差异:机组启停、购售电状态这些是离散整数变量,适合在较长时间尺度上提前确定;而具体出力调整、储能充放电功率是连续变量,更适合在短周期内根据实时情况微调。

这就是双层模型出现的根本原因。上层做长周期、离散决策为主的“计划”,下层做短周期、连续调整为主的“修正”,各管一段,再通过耦合变量衔接起来。如果单层模型是一刀切的“全校统一课表”,那双层模型就是“教务处排总课表,各科老师按实际进度灵活调整”。

1.2 多时间尺度滚动优化到底解决什么问题

讨论滚动优化之前,必须先承认一个事实:预测永远是错的,只是误差大小不同。光伏预测在晴朗稳定的天气下可能误差很小,但遇到多云天气,15分钟级别的出力波动会让人怀疑人生。负荷预测相对稳定,却也存在节假日、气温突变等特殊场景误差。如果调度模型完全依赖日前预测数据,那运行时就只能被动应对偏差,经济性和安全性都会打折扣。

滚动优化(本质上就是模型预测控制,MPC)的思路非常朴素:别一次把24小时全定死,而是每15分钟重新算一次。每次优化只看未来4小时(这个窗口大小可以调),用最新的预测数据求解,然后把结果中第一个15分钟的指令真正下发给设备,剩下的时段只作为参考。等到下一个15分钟到来,窗口向前滑动,重新获取预测、重新求解、再执行第一个时段,如此循环。

这个逻辑和你开车用导航是一样的。出发前导航给了一条全程路线(相当于日前计划),但你真的开起来,不可能全程每一秒都按出发时的路线死板执行,而是每个路口根据实时路况重新规划,只不过导航只给你看接下来几个路口的指引。滚动优化的“前瞻性”来自预测时域,“实时性”来自频繁刷新,“稳定性”则来自只执行第一步的控制策略。

2. 双层模型结构详解:层级怎么分,变量怎么耦合

2.1 上层日前调度模型:离散决策为主

上层模型以1小时为分辨率,覆盖完整的24小时,目标函数是全天总运行成本最小。成本项通常包括:从电网购电的费用、天然气消耗费用(燃气轮机和燃气锅炉)、储能充放电的运行维护成本,以及弃风和切负荷的惩罚成本。如果系统里有需求响应,还可以把柔性负荷的补偿成本写进去。

决策变量分两类。离散变量包括燃气轮机的启停状态、是否购电/售电、可平移负荷的启动时刻等。连续变量包括各时段的燃气轮机出力、储能充放电功率、购售电功率、锅炉供热量等。

约束条件是这个模型的核心,我整理一下典型约束:

约束类型典型表达式说明
电功率平衡P_MT + P_pv + P_wind + P_buy + P_dis = P_load + P_ch + P_sell + P_eb所有电源之和等于所有负荷和消耗之和
热功率平衡H_MT + H_gb + H_tes_dis = H_load + H_tes_ch热源与热负荷/热储能平衡
机组出力上下限u·P_min ≤ P ≤ u·P_max机组停机时出力必须为0
爬坡约束-R ≤ P(t)-P(t-1) ≤ R防止出力突变
储能SOC动态SOC(t+1) = SOC(t) - P_ESS(t)·Δt / Cap注意充放电效率要分开写
购售电互斥0 ≤ P_buy, 0 ≤ P_sell, P_buy ≤ M·(1-u_bs)同一时刻不能同时买卖电

这里最容易犯的错误是把储能充放电效率写成一个固定值。实际上充电和放电过程都有能量损耗,如果SOC动态方程里只用一个效率系数,优化结果中储能会出现“充电立即放电”这种白嫖效率的假象。正确的做法是把储能效率拆成充电效率η_ch和放电效率η_dis,SOC更新公式写成:

SOC(t+1) = SOC(t) - (P_dis(t)/η_dis - P_ch(t)·η_ch)·Δt / Cap

也就是说,放电时SOC下降量要除以放电效率(实际消耗的SOC比输出功率大),充电时SOC上升量要乘以充电效率(实际存入的SOC比输入功率小)。这个细节直接影响成本和SOC曲线的合理性。

2.2 下层日内滚动模型:连续调整为主

下层模型的分辨率通常取15分钟,一天就是96个时段。预测时域(滚动窗口)我习惯取4小时,也就是16个时段。窗口太短,前瞻性不足,风光波动来了来不及调整;窗口太长,预测误差累加又抵消了滚动优化的优势。

下层模型的决策变量几乎全是连续的:各设备出力修正量、储能功率调整量、联络线功率等。整数变量一般很少保留,因为机组启停已经在日前阶段定好了,日内滚动阶段默认机组状态不变,只调整出力大小。这种设计大大降低了日内模型的求解难度,从MILP变成了LP,求解速度可从秒级降到毫秒级。

目标函数的设计有两种主流思路。第一种是“跟踪型”:最小化各设备出力与日前计划值的偏差,让实际运行尽量贴合计划。第二种是“经济型”:最小化从当前时刻到预测时域末端的运行成本加上偏差惩罚。我实际做下来,纯跟踪型在预测误差较大时会导致储能过度补偿,纯经济型又会让设备出力频繁波动,最优的做法是混合式:

min Σ_k [ α_MT·(P_MT(k)-P_MT_ref(k))² + α_ESS·(P_ESS(k)-P_ESS_ref(k))² + β_pen·(失负荷量) ]

其中P_MT_ref和P_ESS_ref来自日前计划,α是跟踪权重,β是失负荷惩罚系数。这样的好处是:慢设备(燃气轮机)尽量平稳跟踪计划,避免机械磨损和热应力;快设备(储能)承担主要的波动平抑任务;极端情况下允许少量切负荷,保证模型不因硬约束而不可行。

2.3 两层之间的信息传递与反馈修正

两层模型不是孤立的,它们通过计划值、状态量和末端约束三个渠道耦合。

第一步,上层求解完成后,把燃气轮机出力序列、储能功率序列、SOC末端值存入一个结构体,比如result_DA.P_MT_ref = value(P_MT),这些就是下层的跟踪基准。第二步,日内滚动优化开始后,每个控制周期读取最新的SOC实际值(由上一个周期的功率执行结果更新),作为当前优化窗口的初始状态。第三步,为了防止滚动窗口的末端SOC偏离日前计划太多,我会在滚动优化的约束里加一个“末端SOC软约束”,让窗口最后时段的SOC尽量接近日前计划在相同时刻的值。

这种“顺序求解+状态反馈”的结构,在数学上不是一个严格的嵌套双层优化(Stackelberg博弈),而是更像“计划-执行-修正”的工程实现。它的优点是计算效率高、收敛性天然有保障,缺点是无法做到理论上的全局最优。如果论文要求严格的上下层联立求解,可以后续用KKT条件把下层问题转成上层约束,或者用迭代法反复传递影子价格,但实际工程复现先跑通顺序结构,再考虑进阶方案是更稳妥的路径。

3. MATLAB代码复现的完整实操过程

3.1 环境准备:YALMIP与求解器配置

复现这个模型最常用的工具箱组合是YALMIP + CPLEX,次选YALMIP + GUROBI。YALMIP是MATLAB下的建模层,负责把优化问题用人类友好的语法写出来,然后转成求解器能识别的标准形式。CPLEX是IBM的商用求解器,对混合整数线性规划(MILP)的求解能力非常强。

安装顺序有讲究。先把YALMIP文件夹下载下来,放到一个固定目录,然后在MATLAB里“设置路径”添加该文件夹。接着安装CPLEX,注意版本兼容性:CPLEX 12.10版本官方支持MATLAB R2017b到R2019b,CPLEX 12.10对应更新的MATLAB版本,一般建议CPLEX 12.10以上搭配R2020a以上。安装完成后在MATLAB里运行yalmiptest,看到CPLEX出现在可用求解器列表里就说明配置成功。

如果手头没有CPLEX和GUROBI的正版授权,退而求其次可以用MATLAB自带的intlinprog(用于MILP)和linprog(用于LP)。但说实话,当模型有几十个二进制变量、几百条约束时,intlinprog的求解速度明显慢于CPLEX一个数量级,滚动优化96个周期每个都要等几秒,总时间会非常煎熬。所以有条件的话还是优先考虑能用的求解器。

3.2 数据准备与参数设置

复现代码的第一步是把论文里的算例参数转成MATLAB的数据结构。我习惯用case结构体来存参数,这样后续函数调用清晰,不容易传参传错。

% 定义典型日数据:此处为示例,24h电负荷曲线(单位kW) caseData.load_elec = [120 115 110 108 105 110 130 160 180 190 200 205 ... 195 185 175 170 180 195 210 220 215 190 155 130]; % 24h热负荷曲线(单位kW) caseData.load_heat = [80 85 90 95 95 90 80 70 65 60 58 55 ... 55 60 65 70 75 80 85 90 95 90 85 80]; % 光伏预测(kW),夜间为0 caseData.pv = [0 0 0 0 0 0 10 35 70 100 120 125 ... 115 95 65 30 10 0 0 0 0 0 0 0]; % 风电预测(kW) caseData.wind = [60 65 70 75 70 65 60 55 50 45 40 35 ... 40 45 55 60 65 70 72 68 64 60 58 55]; % 分时购电价(元/kWh):峰平谷 caseData.price_buy = [0.4*ones(1,8), 0.8*ones(1,4), 1.2*ones(1,8), 0.8*ones(1,4)];

设备参数我单独列一个结构体:

% 燃气轮机参数 dev.P_MT_max = 200; % 最大出力 kW dev.P_MT_min = 20; % 最小出力 kW dev.ramp_MT = 60; % 爬坡速率 kW/h dev.eta_MT = 0.35; % 发电效率 dev.gas_price = 3.0; % 气价 元/m3 dev.LHV_gas = 9.7; % 天然气低位热值 kWh/m3 % 储能参数 dev.Cap_ESS = 500; % 容量 kWh dev.SOC_max = 0.9; dev.SOC_min = 0.1; dev.eta_ch = 0.95; dev.eta_dis = 0.95; % 惩罚成本 dev.penalty_load_loss = 10; % 失负荷惩罚 元/kWh dev.penalty_wind_curtail = 5; % 弃风惩罚 元/kWh dev.c_MT_om = 0.03; % MT运维成本 元/kWh dev.c_ESS_om = 0.02; % ESS运维成本 元/kWh

注意:数据准备直接决定了复现结果和论文是否一致。如果论文里给了算例数据表,一定要逐条核对;如果没有,就用典型日曲线加上正态分布随机扰动来生成,并在代码里注明“数据为示例生成”的说明。

3.3 日前调度模型的YALMIP建模

上层日前调度是一个标准的MILP问题。YALMIP建模的核心步骤是:声明变量 -> 写约束 -> 写目标函数 -> 调用求解器。

% 变量声明 P_MT = sdpvar(24,1); % 燃气轮机出力 u_MT = binvar(24,1); % 启停状态 0/1 P_buy = sdpvar(24,1); % 购电 P_sell = sdpvar(24,1); % 售电 P_ESS_ch = sdpvar(24,1); % 储能充电功率 P_ESS_dis = sdpvar(24,1); % 储能放电功率 P_eb = sdpvar(24,1); % 电锅炉耗电 H_gb = sdpvar(24,1); % 燃气锅炉供热 H_tes_ch = sdpvar(24,1); % 热储能充热 H_tes_dis = sdpvar(24,1); % 热储能放热 SOC = sdpvar(24,1); % 储能SOC u_bs = binvar(24,1); % 购售电状态,1购0售 Constraints = []; % 电功率平衡 for t = 1:24 Constraints = [Constraints, ... P_MT(t) + P_buy(t) + P_ESS_dis(t) + caseData.pv(t) + caseData.wind(t) ... == caseData.load_elec(t) + P_eb(t) + P_ESS_ch(t) + P_sell(t)]; % 热功率平衡 Constraints = [Constraints, ... P_MT(t)*0.5 + H_gb(t) + H_tes_dis(t) ... == caseData.load_heat(t) + H_tes_ch(t)]; end

上面热平衡里P_MT(t)*0.5是简化处理,表示燃气轮机余热回收系数。实际论文里会用余热回收效率和热功率与电功率的联产比来算,这属于热电联产典型“以热定电”或“以电定热”的运行约束,复现时一定要看清论文用的是哪种模式。

机组约束和储能约束如下:

% 机组出力上下限(停机时出力为0) Constraints = [Constraints, ... dev.P_MT_min.*u_MT <= P_MT <= dev.P_MT_max.*u_MT]; % 爬坡约束 Constraints = [Constraints, ... -dev.ramp_MT <= P_MT(2:end) - P_MT(1:end-1) <= dev.ramp_MT]; % 最小启停时间约束(如果需要) % 这里用简化处理,只保证启停状态与出力联动 % 储能约束 Constraints = [Constraints, ... 0 <= P_ESS_ch <= 150, 0 <= P_ESS_dis <= 150]; Constraints = [Constraints, ... SOC(2:end) == SOC(1:end-1) - (P_ESS_dis(1:end-1)./dev.eta_dis - P_ESS_ch(1:end-1).*dev.eta_ch)./dev.Cap_ESS]; Constraints = [Constraints, ... SOC >= dev.SOC_min, SOC <= dev.SOC_max]; Constraints = [Constraints, SOC(1) == 0.5, SOC(24) >= 0.5]; % 始态和末态SOC约束

这里解释一下SOC更新公式为什么这样写。把Δt=1小时代入,SOC(t+1) = SOC(t) - (P_dis/η_dis - P_ch·η_ch)/Cap。放电时,由于η_dis<1,P_dis/η_dis > P_dis,意味着SOC下降得比输出功率对应的“理想值”更快,这部分损耗就是电池内部电阻的热损耗。充电时,P_ch·η_ch < P_ch,意味着SOC上升得比输入功率对应的“理想值”更慢。这样写才能在目标函数里准确反映储能的充放电损耗。

目标函数:

% 目标函数: 购电费 + 购气费 + 运维费 - 售电收入 + 惩罚项 Objective = sum(caseData.price_buy .* P_buy) ... + sum(dev.gas_price ./ dev.LHV_gas .* (P_MT ./ dev.eta_MT + H_gb / 0.8)) ... + sum(dev.c_MT_om .* P_MT) + sum(dev.c_ESS_om .* (P_ESS_ch + P_ESS_dis)) ... - sum(caseData.price_sell .* P_sell) ... + sum(dev.penalty_wind_curtail .* (caseData.wind - wind_used)) ... + sum(dev.penalty_load_loss .* load_loss);

注意燃气轮机燃气费用的计算逻辑:P_MT是输出的电功率,除以发电效率η_MT后得到输入功率,再乘以气价和热值的换算关系才是燃气费用。很多第一次复现的人在这里直接把P_MT乘气价,结果成本完全对不上。

求解命令:

ops = sdpsettings('solver','cplex','verbose',1); result = optimize(Constraints, Objective, ops); if result.problem == 0 % 求解成功 result_DA.P_MT_ref = value(P_MT); result_DA.P_ESS_ref = value(P_ESS_dis) - value(P_ESS_ch); result_DA.SOC_ref = value(SOC); else disp('Day-ahead optimization failed, check constraints'); end

3.4 滚动优化主循环的实现

滚动优化是代码复现的重头戏,也是从“能跑”到“跑得对”的分水岭。主循环结构如下:

T_total = 96; % 一天96个15min时段 H = 16; % 预测时域:4小时 dt = 0.25; % 每个时段的时长,小时 % 将日前计划的1h序列插值成15min序列(或者用hold方式扩展) P_MT_ref_15min = expand_1h_to_15min(result_DA.P_MT_ref); P_ESS_ref_15min = expand_1h_to_15min(result_DA.P_ESS_ref); SOC_ref_15min = expand_1h_to_15min(result_DA.SOC_ref); % 初始化结果矩阵和当前状态 result_IR = zeros(T_total, 6); % 记录: P_MT, P_ESS, P_buy, SOC, wind_used, load_shed SOC_now = 0.5; for k = 1:T_total % 1. 构造当前窗口的预测数据(加上随机扰动模拟预测误差) idx = k:min(k+H-1, T_total); pv_pred = caseData.pv_15min(idx) + randn(length(idx),1)*5; load_pred = caseData.load_15min(idx) + randn(length(idx),1)*10; % 如果k+H-1超过了96,剩下的时段用最后一段数据填充 if length(idx) < H pv_pred = [pv_pred; caseData.pv_15min(end)*ones(H-length(idx),1)]; load_pred = [load_pred; caseData.load_15min(end)*ones(H-length(idx),1)]; end % 2. 调用滚动窗口求解函数 [P_MT_seq, P_ESS_seq, P_buy_seq, load_shed_seq, wind_used_seq] = ... solve_rolling_window(k, pv_pred, load_pred, SOC_now, ... result_DA, ...); % 3. 只执行第一个时段 result_IR(k,1) = P_MT_seq(1); result_IR(k,2) = P_ESS_seq(1); result_IR(k,3) = P_buy_seq(1); result_IR(k,4) = SOC_now; result_IR(k,5) = wind_used_seq(1); result_IR(k,6) = load_shed_seq(1); % 4. 更新SOC状态 if P_ESS_seq(1) > 0 SOC_now = SOC_now - (P_ESS_seq(1)/dev.eta_dis)*dt/dev.Cap_ESS; else SOC_now = SOC_now - (P_ESS_seq(1)*dev.eta_ch)*dt/dev.Cap_ESS; end end

滚动窗口内部函数solve_rolling_window的建模逻辑与日前类似,但有三个关键区别。第一,目标函数里加入了跟踪项,用日前计划的插值作为参考值。第二,SOC初始值是传入的实际SOC_now,不是固定值。第三,窗口末端SOC要尽量靠近SOC_ref_15min(k+H-1),这个用软约束实现,即允许偏离但要在目标函数里惩罚。

function [P_MT_seq, P_ESS_seq, P_buy_seq, load_shed_seq, wind_used_seq] = ... solve_rolling_window(k, pv_pred, load_pred, SOC_now, ... P_MT_ref, P_ESS_ref, SOC_ref, dev, dt) % 声明窗口内变量,维度为H P_MT = sdpvar(H,1); P_ESS_ch = sdpvar(H,1); P_ESS_dis = sdpvar(H,1); P_buy = sdpvar(H,1); SOC = sdpvar(H,1); load_shed = sdpvar(H,1); wind_used = sdpvar(H,1); Constraints = []; % 电功率平衡(含弃风、切负荷变量) for t = 1:H Constraints = [Constraints, ... P_MT(t) + P_buy(t) + P_ESS_dis(t) + pv_pred(t) + wind_used(t) ... == load_pred(t) + P_ESS_ch(t) - load_shed(t)]; % 弃风和切负荷限制 Constraints = [Constraints, 0 <= wind_used(t) <= pv_pred(t)*0 + 0]; % 此处wind_used上限应该由风电预测决定,注意变量含义 Constraints = [Constraints, 0 <= load_shed(t) <= load_pred(t)*0.1]; end % SOC动态 Constraints = [Constraints, SOC(2:end) == SOC(1:end-1) ... - (P_ESS_dis(1:end-1)./dev.eta_dis - P_ESS_ch(1:end-1).*dev.eta_ch).*dt./dev.Cap_ESS]; Constraints = [Constraints, SOC(1) == SOC_now]; Constraints = [Constraints, SOC >= dev.SOC_min, SOC <= dev.SOC_max]; % 机组出力范围(假设日前已确定启停状态,这里出力上下限固定) Constraints = [Constraints, dev.P_MT_min <= P_MT <= dev.P_MT_max]; % 爬坡约束(注意跨窗口衔接) Constraints = [Constraints, ... -dev.ramp_MT*dt <= P_MT(2:end) - P_MT(1:end-1) <= dev.ramp_MT*dt]; % 目标函数:运行成本 + 跟踪偏差惩罚 + 末端SOC软约束 Objective = sum(dev.c_MT_om .* P_MT) ... + sum(caseData.price_buy(k:k+H-1) .* P_buy) ... + sum(dev.penalty_load_loss .* load_shed) ... + 0.5*sum((P_MT - P_MT_ref).^2) ... + 0.3*sum((P_ESS_dis - P_ESS_ch - P_ESS_ref).^2) ... + 20*(SOC(end) - SOC_ref).^2; ops = sdpsettings('solver','cplex','verbose',0); result = optimize(Constraints, Objective, ops); if result.problem ~= 0 % 如果不可行,进行降级处理:减少跟踪权重,放宽约束 warning('Rolling window %d infeasible, relax constraints', k); end ... end

这段代码里有一个我在复现时经常发现的错误点:滚动窗口的爬坡约束。由于每个窗口内的P_MT序列是重新生成的,第2个时段的出力与上一个窗口已经执行过的实际出力存在衔接关系。我在循环外维护了一个last_MT变量,在窗口内加一条约束:P_MT(1)与last_MT的差值必须在爬坡范围内。如果不加这条约束,第一个时段的出力可能在上一时段的基础上突然跳变,实际物理上根本不可行。这个细节很多论文里不会写,完全是工程实现的经验。

3.5 结果输出与可视化

复现完成后,核心是验证结果的合理性。我通常画三组图:第一组是日前计划与实际执行的功率对比曲线,重点看燃气轮机和储能是否在跟踪基准附近合理波动;第二组是SOC在一天内的变化轨迹,检查是否始终在[0.1, 0.9]范围内,且末端回到0.5附近;第三组是购售电功率和各个成本项的堆叠图。

figure; stairs(1:24, result_DA.P_MT_ref, 'linewidth', 1.5); hold on; stairs(1:96, result_IR(:,1), 'linewidth', 1); legend('日前计划MT出力','日内实际MT出力'); xlabel('时段'); ylabel('功率(kW)'); title('燃气轮机出力:日前计划 vs 日内滚动');

如果发现日内实际出力曲线和日前计划偏差很大,说明跟踪权重设置得太小;如果完全重合,说明滚动优化没有起到应对预测误差的作用。理想状态是:曲线整体贴合计划,但在预测误差大的局部时段有明显的修正动作。

4. 常见问题与排查技巧实录

4.1 求解器相关的坑

YALMIP路径冲突是最常见的问题。我遇到过装了多个工具箱之后,MATLAB里出现多个名为“optimize”或“check”的函数,导致YALMIP调用出错。排查方法是在命令窗口输入which optimize,看看实际调用的文件路径是哪一个,不是YALMIP目录下的就有问题。

另一个典型问题是CPLEX求解器报license错误或者找不到优化引擎。Windows下安装CPLEX后,打开MATLAB运行yalmiptest,如果CPLEX显示为“not available”,检查环境变量PATH是否包含CPLEX的bin目录。很多人在path里添加的是CPLEX的matlab文件夹但不是bin目录,导致求解器找不到执行文件。

4.2 不可行解的排查方法

遇到Infeasible问题,我的排查顺序是固定的。第一步,检查约束维度是否匹配。YALMIP里sdpvar变量维度写错,约束拼接时就会出现“维度不一致”的错误,这种问题通常在代码运行前就会被MATLAB提示出来。第二步,检查SOC约束。SOC上下限、充放电功率上限、初始和末端SOC之间是否存在矛盾。比如SOC从0.5开始,末端要求回到0.5,但储能容量只有500kWh,而期间需要充放电的能量远超过储能容量,这种能量不平衡必然导致不可行。第三步,用诊断工具定位:

ops = sdpsettings('solver','cplex','verbose',1,'debug',1); optimize(Constraints, Objective, ops);

debug=1模式会让YALMIP尝试分析冲突约束,输出不可行约束的编号和变量边界。如果不想用debug,也可以手动把约束分组注释掉,用二分法缩小不可行范围。

4.3 滚动优化结果不合理的修正

“结果不合理”分为几种情况。购售电同时为正,或者联络线功率剧烈振荡,这是缺少购售电互斥约束或没有对联络线功率变化速率加以限制。储能频繁在充电和放电之间切换,说明储能运维成本或循环次数惩罚没有设置好,目标函数没有理由让储能“少动”。燃气轮机出力呈现锯齿状,则可能是爬坡约束没有跨窗口衔接好,或者跟踪权重过小。

最让我头疼的一个问题发生在光伏预测误差模拟环节。一开始我用randn生成独立高斯扰动,结果光伏在夜间出现负值,导致模型为了平衡这个“假电源”而做出奇怪决策。后来改成在白天时段用比例误差:

pv_pred = pv_base .* (1 + 0.1*randn(length(idx),1)); pv_pred(pv_pred < 0) = 0;

这样误差和出力水平成比例,更符合光伏预测误差的实际分布特性。

4.4 双层迭代收敛性的工程化处理

如果你决定使用严格的KKT条件转换法,需要面对双线性项导致的问题。下层问题的KKT条件里包含互补松弛条件,其中“λ·(约束边界)”这类乘积项是非线性的。处理方法是引入二进制变量和大M法线性化,但这会大幅增加问题的规模和求解难度。我建议先跑通顺序迭代结构,再评估是否值得转成单层。在顺序迭代结构中,偶尔会出现上层计划与下层实际执行偏差累计过大的情况,解决方法是每滚动若干周期后,以上一轮结果作为新的参考基准重新求解上层模型,形成外循环反馈。

另外一个工程化技巧是:在两层之间加一个“偏差上限”约束。当日内某个设备的出力与日前计划偏差超过一定范围(比如20%),强制调整购电或切负荷来消化,避免设备在极端工况点反复振荡。这个机制相当于给滚动优化的自由度上了一道保险。

5. 个人实操中的一些体会

这套代码我前后调了两周才稳定跑完96个时段不报错。最大的体会是:论文里的模型公式只是冰山一角,真正实现时那些“不言自明”的假设才是你花时间的重头戏。比如爬坡约束跨窗口衔接、储能SOC末端软约束以及目标函数里跟踪权重的量纲,这些在论文里往往一句话带过,但不处理到位,程序要么跑不动,要么结果看起来完全不合理。

再分享一个具体的小技巧:调试滚动循环时,先用H=4的小窗口跑通,日志打印每个窗口的目标函数值和求解时间,确认逻辑无误后再放大到H=16。这个做法能把索引溢出、维度不匹配这类低级错误快速捞出来,避免等到全量运行才暴露,到时候找bug的成本高得多。

这套模型后续扩展的空间也很大。可以在下层加入日前实时电价预测,或者在滚动优化里考虑储能寿命的实时成本,甚至把热力系统的动态延迟特性建模进去。无论如何,先把“日前计划+日内滚动”这个主干跑透,后面加什么都顺理成章。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询