☰
基于Copula与热泵虚拟储能的Matlab风光波动平抑MPC实战
2026/10/28 10:32:06 网站建设 项目流程

风电光伏现在装机量上来了,但并网之后的波动问题一直让人头疼。白天光伏猛出力,晚上风电接力,净负荷曲线经常像坐过山车。我最近做的这个项目,核心思路就是利用热泵这种柔性负荷去平抑风电光伏联合出力的波动。这中间有两个坎绕不开:一个是风电和光伏出力之间的相关性怎么描述,另一个是热泵的建筑热惯性怎么建模、怎么丢进优化器里求解。整条链路我用Matlab完整实现了一遍,从Copula相关性建模、场景生成,到热泵虚拟储能建模、MPC滚动优化,代码全部跑通。这篇就当成一次踩坑记录和经验分享,给准备做新能源波动平抑、灵活性资源调度的同学一个可以“抄作业”的参考。

先说适用范围:如果你手头有风光出力历史数据,想研究热泵、空调这类温控负荷参与削峰填谷,或者想在Matlab里搭建随机优化/模型预测控制仿真,那么这篇文章应该对你有帮助。我尽量把原理部分讲得接地气一点,代码部分能直接改来用。下面按我实际开发的顺序来讲。

1. 项目整体思路与设计拆解

1.1 为什么波动平抑总也绕不开相关性问题

很多人做风光出力波动分析时,习惯把风电和光伏单独建模,再简单相加。这个做法有个隐患:风电和光伏出力并不是独立变量。同一个区域内的风资源和光照资源,往往受同一条天气系统影响,比如强冷锋过境时大风伴随云量大增,风电出力可能飙升、光伏出力却被打压;再比如连续阴雨天,光伏出力低,风可能也不大。如果只按独立变量去生成场景,就会低估“风电光伏同时处于低出力状态”的概率,优化出来的平抑策略偏乐观,实际跑起来联络线功率照样剧烈波动。

我在这套方案里引入Copula,就是为了把“各自出力的概率分布”和“两个变量之间的相关结构”分开建模。Copula族里有很多成员,高斯Copula、t-Copula、Clayton、Gumbel等,它们能表达不同形态的相关关系,尤其是尾部相关性。对风电光伏这种存在极端同步事件的场景,用t-Copula通常比单纯用线性相关系数可靠得多。

1.2 热泵凭什么能当“虚拟电池”

热泵这东西和电热水器、普通空调不一样的地方在于,它不只是把电能直接转化成热能,而是把室外低温热源里的热量“搬运”进室内,能效比COP一般有3左右,也就是消耗1度电可以往房间里搬3度热。与此同时,建筑本身是有热惯性的——墙体、楼板、家具、空气共同构成一个巨大的蓄热体。

这个特性给了我们一个很好的可调节空间:短时间让室内温度稍微升高一点,热泵就可以少出力,甚至停机;反过来,提前把房间加热到舒适区间的上限,热泵就可以暂缓运行。整个过程相当于把建筑当成了一个“虚拟电池”,温度上升等于充电,温度回落等于放电。相比电化学储能,热泵的调节成本低得多,而且它本身就是负荷,不会产生额外弃风弃光压力。只要允许室内温度在人体舒适范围内波动(比如20到24摄氏度),热泵的功率调节范围就相当可观。

1.3 整体技术路线(从数据到代码)

整套项目我拆成了五个模块,分头实现,最后再串起来跑。

第一步,从历史数据拟合风电光伏出力的边缘分布,再用Copula拟合它们之间的相关结构。第二步,利用拟合好的Copula生成大量联合出力场景,再通过聚类缩减得到有代表性的场景集。第三步,建立热泵的一阶热动态模型,把建筑热惯性和温度舒适区间转化为优化约束,等价成虚拟储能模型。第四步,建立以“平抑联络线功率波动”为目标的优化模型,在Matlab中用Yalmip调用求解器求解。第五步,把模型放进模型预测控制(MPC)的滚动框架里,在每个控制周期重新求解、只执行第一个控制量。

这五步里,相关性建模和热泵模型是整篇文章的核心,优化求解是最终落地的手段。我强烈建议不要一上来就狮子大开口搞随机MPC,先把确定性模型跑通,再往里面灌Copula场景。

2. Copula相关性建模与场景生成实现

2.1 Copula的核心思想:边缘分布与相关结构解耦

Copula的思想可以简化成一句话:任意多个随机变量的联合概率分布,都可以拆成每个变量的边缘分布,外加一个描述“变量之间如何纠缠”的连接函数。用Sklar定理写出来就是:

H(x1, x2) = C(F1(x1), F2(x2))

其中F1和F2是风电、光伏出力的边缘累积分布函数,C就是Copula函数。这一步拆分的价值非常大:我可以先单独把风电出力的概率分布拟合到尽可能准,再用另一个合适的Copula去描述“风电低出力的时候光伏是不是也低出力”,两者互不干扰。

传统做法里,大家一说相关性就上Pearson相关系数,但它只能捕捉线性相关。风电光伏之间的关系经常是非线性的,而且极端情况下的相关性和平均情况下的相关性完全不同。比如平均来看风光可能负相关,但遇到极端天气时两个出力同时掉到很低的水平,这种“尾部相关”Pearson系数反映不出来。Clayton Copula擅长描述下尾相关,Gumbel Copula擅长描述上尾相关,t-Copula是两头都有尾部相关,选择哪一个要看数据实际长什么样。

2.2 Matlab中拟合与采样代码详解

我在Matlab里实现时,用了统计工具箱里现成的copulafit和copularnd,自己主要处理边缘分布。下面这段是我项目里的核心代码,加了详细注释。

% 加载历史数据,第一列归一化风电出力,第二列归一化光伏出力 load('wind_pv_data.mat'); % 变量名 data,8000个样本点 wind = data(:,1); pv = data(:,2); % 1) 边缘分布拟合:用核密度估计,不假定正态分布 [~, wind_cdf] = ksdensity(wind, 'Function', 'cdf', 'Bandwidth', 0.02); [~, pv_cdf] = ksdensity(pv, 'Function', 'cdf', 'Bandwidth', 0.02); % 2) 转换为均匀分布样本 u_wind = wind_cdf(wind); u_pv = pv_cdf(pv); U = [u_wind, u_pv]; % 3) 拟合Gaussian Copula和t Copula rho_g = copulafit('Gaussian', U); [rho_t, nu_t] = copulafit('t', U); % 4) 生成2000个联合概率场景 U_sim = copularnd('t', rho_t, nu_t, 2000); % 5) 逆变换回出力场景 wind_sim = ksdensity(wind, U_sim(:,1), 'Function', 'icdf', 'Bandwidth', 0.02); pv_sim = ksdensity(pv, U_sim(:,2), 'Function', 'icdf', 'Bandwidth', 0.02);

这里我必须提醒一个特别容易踩的坑:ksdensity做逆变换时,如果U_sim里有值恰好在0或者1附近,icdf计算可能会返回NaN或者Inf。我处理的办法是,生成完U_sim之后先做一次截断,把所有小于0.001的值改成0.001,大于0.999的值改成0.999。

另外,带宽Bandwidth的选择对拟合结果影响很大。带宽设得太大,边缘分布被过度平滑,关键尖峰丢失;设得太小,又会过拟合噪声。我习惯先用ksdensity默认带宽,再画图对比经验CDF和拟合CDF,如果偏差明显再手动调整。

2.3 场景缩减:从2000个场景到几十个代表场景

Copula生成2000个甚至更多场景很容易,但优化模型不可能直接把这2000个场景全部灌进去,不然求解时间会爆炸。所以实际工程里一定要做场景缩减。

我用的方法是K-means聚类。把每个场景看成二维点(风电出力,光伏出力),聚类之后每个簇的中心就是代表性场景,簇内场景数量占总场景数的比例就是该代表场景的概率。代码非常简单:

n_rep = 30; [idx, center] = kmeans([wind_sim, pv_sim], n_rep, 'Replicates', 10); probs = zeros(n_rep, 1); for k = 1:n_rep probs(k) = sum(idx == k) / length(idx); end

聚类缩减有一个值得注意的地方:K-means会倾向于保留数据密集区域的代表点,尾部极端场景可能被合并掉。如果你要研究的恰恰是极端场景下的波动,建议在聚类之前先对极端场景做加权复制,或者在聚类之后额外保留一部分最极端的原始场景。比如我就在缩减后的场景集里加了两个手挑的“最坏场景”:风光同时高出力、风光同时低出力,用来做鲁棒性校验。

2.4 实操注意:边缘分布比Copula本身更影响结果

我做了好几轮实验后得出一个结论:如果你把边缘分布拟合得很粗糙,那后面换什么Copula都救不回来。边缘分布决定了每个变量自己的概率形态,Copula只负责把它们“拧”在一起。如果风电出力本身拟合得就偏,生成场景的边缘统计量自然就是错的。

实操上我建议做三件事:

第一,对原始数据先做清洗,去掉异常值和停机时段的数据,不然拟合参数会被带偏。第二,按季节分别拟合Copula参数。冬春季和夏秋季的风光相关结构差异很大,全年混在一起拟合,会得到一个“平均状态”,极端情况反而被抹平。第三,用QQ图检查生成场景的边缘分布和原始数据是否一致。如果差异明显,多半是边缘分布或者逆变换环节出了问题。

还有一个容易忽略的点:这里生成的场景是“独立同分布”快照,没有时间连续性。如果要做MPC,需要的是未来多个时段的时序场景。我的做法是先用预测模型得到每个时段的均值,再在均值上叠加Copula生成的随机扰动,从而构造时序场景。你也可以用向量自回归或者马尔可夫链来引入时序相关,但复杂度会高很多,初期不建议上手就搞。

3. 热泵灵活性建模与约束处理

3.1 热泵功率与室内温度的耦合关系

热泵的灵活性来自于建筑热惯性,所以建模时不能把热泵当普通电负荷,要把它和室内温度挂钩。我这里采用一阶RC等效模型,虽然简单,但工程上已经足够刻画“虚拟储能”特性。

模型长这样:

C * dTin/dt = Q_hp - (Tin - Tout) / R

其中C是建筑等效热容,单位kWh/K,代表建筑蓄热能力的强弱;R是建筑等效热阻,单位K/kW,代表围护结构保温性能。Q_hp是热泵制热功率,单位kW;Tin和Tout分别是室内外温度。

热泵电功率P_hp和热功率Q_hp之间用COP联系:Q_hp = COP * P_hp。COP不是一个固定值,实际会随室外温度、热泵部分负荷率变化,但作为论文级仿真,可以先取一个随室外温度分段变化的值,后面我再讲怎么处理。

离散化之后得到差分方程:

Tin(t+1) = Tin(t) + (dt / C) * (COP * P_hp(t) - (Tin(t) - Tout(t)) / R)

这里dt是采样周期。注意当C很小、dt很大的时候,欧拉离散容易数值不稳定,我建议采样周期选15分钟到1小时之间,C按实际建筑参数取,一般在0.2~1.0 kWh/K之间。

3.2 如何把“舒适温度范围”变成优化约束

热泵的灵活性说白了就是:室内温度允许在Tmin和Tmax之间浮动,所以热泵功率可以在这个前提下自由调节。把上面差分方程里的Tin限制在[ Tmin, Tmax ]区间,热泵功率限制在[ 0, P_hp_max ],就构成了一个线性可行域。

从储能角度看,温度偏差可以近似看作电池的荷电状态。温度越高,说明建筑“蓄的热越多”,相当于电池电量越足;温度越接近Tmin,相当于电池快放空了。这个视角对理解后续平抑策略很有帮助:光伏大发、功率富余时,我们让温度升高,相当于给“虚拟电池”充电;夜间风电出力不足时,让温度自然回落,热泵少用电,就相当于“放电”支撑净负荷。

构建优化模型时,我建议把温度舒适度限制作为硬约束。如果你担心硬约束导致优化不可行,也可以把它改成软约束,在目标函数里加一个温度越限惩罚项。但从我的测试结果看,只要参数设置合理,硬约束MPC在绝大多数情况下都能正常求解,而且求解速度更快。

3.3 离散化参数与COP处理技巧

这里给出一个典型参数表,方便新手起步:

参数含义典型取值备注
C建筑等效热容0.5 kWh/K越大蓄热能力越强
R建筑等效热阻0.1 K/kW越大保温越好
COP热泵能效比2.5~4随室外温度变化
P_hp_max热泵最大电功率6 kW常规户式热泵功率
T_min / T_max温度舒适区间20 / 24 °C可根据用户偏好调整
dt控制/采样周期0.25~1 h与预测数据粒度匹配

COP的非线性处理我是这么做的:如果用的是确定性MPC,直接把COP设成常数,比如3.0,结果用于方案对比完全没问题。如果要做更精细的仿真,就把室外温度范围分成几段,每段对应一个COP常数,这样模型仍然是线性的。只有当你要把热泵的启停状态、变工况性能都考虑进去时,才需要引入整数变量,那模型就变成混合整数规划,求解时间会明显增加。

4. 平抑优化模型与Matlab求解方案

4.1 目标函数的三种写法与选择

优化目标决定了“平抑”的效果。我试过三种主流写法,各有适用场景。

第一种,联络线功率相对滚动平均值的偏差平方和最小。这个目标最直观,能让联络线功率尽量稳定在某个参考值附近,形成一个相对平直的功率带,适合希望并网功率平稳的场景。

第二种,相邻时段联络线功率变化量绝对值之和最小。这个目标直接压爬坡率,对电网调度最友好,但可能出现“整体抬高/降低”的副作用,优化结果虽然平滑,但联络线平均功率会漂移。

第三种,把功率波动方差和热泵功率调节代价加权组合。比如 obj = sum((P_tie - P_ref).^2) + lambda * sum((P_hp(t) - P_hp(t-1)).^2)。这个方案最灵活,但lambda需要调参,调不好容易牺牲平抑效果。

我项目里选的是第一种,也就是最小化联络线功率相对预测区间内平均值的偏差平方和。原因很简单:它的结果是二次规划(QP),求解器能用得很好,而且物理含义清晰,评审和读者一看就懂。如果后续要加经济性目标,再往里面加电费项或者调节代价项也不难。

4.2 约束条件清单与建模细节

优化模型的决策变量是未来N个时段的P_hp序列,以及对应的室内温度序列Tin。约束分为几类:

功率限值约束,每个时刻热泵电功率在0到P_hp_max之间。温度舒适约束,每个时刻室内温度在Tmin到Tmax之间。热动态等式约束,Tin(t+1)由Tin(t)和P_hp(t)决定,这是模型的关键。爬坡约束,相邻时刻P_hp变化量的绝对值不超过某个限值,防止热泵频繁启停。

如果场景集要引入优化,那么对每个场景s,联络线功率P_tie_s(t)都要参与目标函数计算。P_tie_s(t) = P_base(t) + P_hp(t) - P_wind_s(t) - P_pv_s(t)。注意此时P_hp是公共决策变量,不受场景影响,而P_tie和温度状态是场景相关的。温度状态在不同场景下应该不同,因为热泵相同的电功率下,不同风光场景不会影响室内温度。严格说,温度关于场景独立,只要热泵功率相同温度就相同,所以不确定性只通过P_tie影响目标函数。

4.3 基于Yalmip的求解实现代码

我建议所有涉及优化模型的新手都去装Yalmip,它是Matlab下的建模层,代码写起来直观很多,换求解器也方便。我这里给出确定性MPC的核心代码,场景版只需要把P_wind_pred和P_pv_pred换成多个场景即可。

% 参数 N = 24; % 预测时域 dt = 1; % 1小时 C_b = 0.5; % 建筑热容 kWh/K R_b = 0.1; % 建筑热阻 K/kW COP = 3.0; Tmin = 20; Tmax = 24; T_init = 22; P_hp_max = 6; P_base = 8 * ones(N, 1); % 基础电负荷 % 风光出力预测序列(来自Copula场景平均) P_wind_pred = ...; P_pv_pred = ...; % 室外温度预测 T_out = ...; % 决策变量 P_hp = sdpvar(N, 1); T_in = sdpvar(N, 1); % 联络线功率 P_tie = P_base + P_hp - P_wind_pred - P_pv_pred; P_ref = mean(P_tie); % 目标函数:波动最小 obj = sum((P_tie - P_ref).^2); % 约束 Constraints = []; Constraints = [Constraints, 0 <= P_hp <= P_hp_max]; Constraints = [Constraints, Tmin <= T_in <= Tmax]; for t = 1:N if t == 1 Constraints = [Constraints, ... T_in(1) == T_init + dt/C_b * (COP*P_hp(1) - (T_init - T_out(1))/R_b)]; else Constraints = [Constraints, ... T_in(t) == T_in(t-1) + dt/C_b * (COP*P_hp(t-1) - (T_in(t-1) - T_out(t))/R_b)]; end end % 求解 options = sdpsettings('solver', 'gurobi', 'verbose', 0); optimize(Constraints, obj, options); P_hp_opt = value(P_hp); T_in_opt = value(T_in);

如果你的机器没装Gurobi或Cplex,可以先退回Matlab自带的quadprog,把solver改成quadprog,小规模N=24求解完全没问题。但注意用quadprog时,Yalmip需要等式约束个数不能太少,否则求解器可能会报“非严格凸”之类的警告,这时候把目标函数改成obj = 1e-3 * sum(P_hp.^2) + sum((P_tie - P_ref).^2),加一点正则项就好。

4.4 模型预测控制(MPC)滚动框架

MPC的本质是走一步看一步、滚动更新。在每个控制周期k,根据最新预测和当前室内温度测量值,求解未来N时段的优化问题,但只执行第一个时段的P_hp指令。等到了k+1时刻,用新的状态重新求解。这个闭环结构能抵抗预测误差和模型误差,比一次性开环优化稳健很多。

滚动循环的Matlab框架大致是这样:

for k = 1:total_steps % 更新预测序列(可以用Copula生成的条件期望) P_wind_pred = forecast_wind(k:k+N-1); P_pv_pred = forecast_pv(k:k+N-1); T_out_pred = forecast_temp(k:k+N-1); % 更新当前室内温度测量值(这里用上一轮仿真得到的T_in_actual) T_init = T_in_actual(k); % 求解优化问题,得到P_hp_opt P_hp_opt = solve_mpc(P_wind_pred, P_pv_pred, T_out_pred, T_init); % 执行第一个控制量 P_hp_cmd(k) = P_hp_opt(1); % 用真实模型仿真得到实际室内温度(可加噪声) T_in_actual(k+1) = T_in_actual(k) + dt/C_b * (COP*P_hp_cmd(k) - (T_in_actual(k) - Tout_actual(k))/R_b); end

我实际跑下来,N=24、单场景的QP求解单步在几十毫秒到几百毫秒之间,完全可以在线运行。如果引入30个Copula场景,求解时间会涨到一两秒,也还在可用范围内。

5. 仿真结果分析与关键调试经验

5.1 实验设置与结果对比维度

我为了验证“Copula相关性建模有没有用”“热泵灵活性有没有用”,设计了三组对照组:

A组:无热泵调节,联络线功率就是基础负荷减去风光出力,模拟电网被动消纳。B组:有热泵调节,但生成预测场景时假设风光独立,也就是用边缘分布各自采样再随机配对。C组:有热泵调节,并且用Copula拟合相关结构生成联合场景。

评价指标我用了联络线功率标准差、30分钟内最大爬坡速率、室内温度越限次数、以及求解时间。标准差反映整体波动大小,最大爬坡反映最恶劣采样间隔内的波动冲击。

5.2 从结果能看出什么

从我的仿真结果看,A组到B组的改善是显著的,热泵把一部分高频波动吸收掉了,联络线功率标准差大概下降了20%到35%,具体数值取决于建筑热容C和COP。C组和B组相比,平均波动指标不一定有特别大的差别,但在极端场景下的最大爬坡明显更小了。

这个现象很好解释:B组的场景集没有刻画“风光同时低出力”的尾部相关性,因此优化器认为最恶劣情况没那么严重,给出的热泵功率指令在真正遇到低风光场景时不够“留有余量”。而C组因为场景里包含了这些极端联合事件,MPC会自动在预测到可能出现“风光同低”时提前多蓄一些热,从而把最坏情况下的爬坡压下来。所以Copula的价值不是让平均指标更好看,而是让策略对真实世界的极端情况更稳健。

5.3 排错与调试:我踩过的几个坑

做这个项目的过程中,我前前后后踩了不少坑,有些坑不调试根本发现不了。挑几个典型的列在下面。

现象可能原因解决方法
ksdensity逆变换出现NaN生成的概率值接近0或1对U_sim做截断,限制在[0.001,0.999]
Yalmip提示找不到求解器没装Gurobi/Cplex先换quadprog测试,小规模场景够用
MPC运行一段时间温度越限只用上一次最优解当初始状态,模型漂移引入温度测量反馈,实时修正T_init
Copula场景进入优化后求解很慢场景数量太多用K-means缩减到30个以内
优化结果联络线功率整体偏移目标函数只压波动没有压平均功率增加P_ref固定约束或加入P_hp正则项
热泵功率频繁剧烈变化缺少爬坡约束添加abs(P_hp(t)-P_hp(t-1))<=DeltaP_max

除了表格里的问题,还有一个模型层面的坑想特别提醒:建筑热容C和热阻R如果拍脑袋给,会导致热泵灵活性被严重高估或低估。我一开始把C设得很大,结果是热泵能“自由充放电”很长时间,优化出来的平抑效果漂亮得离谱。后来我用最小二乘法对实测室内温度数据做参数辨识,才让优化结果回到合理水平。

另外,场景缩减那一步,K-means聚类做出来之后一定要验证一下缩减前后场景集的“覆盖度”。我遇到过聚类中心全挤在一起、把风光同时高的极端场景丢掉的情况,导致优化结果在极值时刻失去保护。后来我在缩减场景里手动加入最坏场景,问题才解决。

最后再聊一个我后来加进去的小扩展:把Copula参数从固定离线值改成随时间滚动的在线估计。每来一个新数据,就更新边缘分布和Copula参数,场景集也跟着变。我实测在40个场景、24步预测时,单步MPC求解时间不超过两秒,整个流程完全具备在线运行的条件。这个项目做到后面,我最大的感受是:Matlab写这类优化仿真,真正花时间的不是优化代码本身,而是数据清洗、参数辨识和场景生成前处理。把这部分做扎实,优化模型哪怕简单一点,结果也会很稳。希望这篇文章能帮你少走点弯路。

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

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

立即咨询