“计及碳排放交易及多种需求响应的微网/虚拟电厂日前优化调度”这个题目,我这两年见过太多次了,本科毕设、硕士小论文、电网项目复现,几乎每个做园区微网和虚拟电厂的人都会撞上它。大部分请教我的同学其实卡在同一个地方:模型到底怎么搭、碳排放交易怎么和调度耦合进去、需求响应建模建得不像回事、Matlab里写了半天约束结果求解器报 infeasible。
这篇我就按自己的复现经验,从模型拆解讲到Yalmip代码实现,再讲坑,尽量让你看完能直接对着改出自己的算例。无论你是刚入门还是已经有一定基础,只要是围绕微网/虚拟电厂做日前优化仿真的人,这篇文章都值得存一下。
1. 先从“调度问题”本身说起
1.1 微网/虚拟电厂为什么非要“日前”优化
所谓日前调度,就是提前一天把未来24小时(或者96个15分钟时段)的发电计划、储能充放电计划、购售电计划、负荷调整计划全部定下来。做计划的前提是未来24小时的负荷、光伏、风电都有预测曲线,然后在这个预测基础上,通过优化计算,找到一个“明天怎么运行最省钱”的方案。
微网最大的特点是麻雀虽小五脏俱全,光伏、风机、燃气轮机、储能、负荷,可能还连着上级配电网。要是每台设备都自己管自己,不考虑协调,结果常常是白天光伏大发时电送不出去,晚高峰又高价从网购电,储能也永远充不满放不干净。日前调度的价值,就是把所有设备当成一个整体,让燃气轮机知道什么时候出力、让储能知道什么时候该充、什么时候该放,让可调负荷主动避开高价时段,最终让系统总运行成本最低。
虚拟电厂和微网的区别在于,它不强求物理上的孤岛或并网点,更像一个聚合商,把分散的屋顶光伏、充电桩、空调、储能聚合起来参与电网互动。不过落到日前优化调度这一步,数学上两者核心几乎一样,都是对可控资源做时序协调,只是虚拟电厂会更强调和外部电力市场交易互动。所以你看到很多论文标题直接写成“微网/虚拟电厂”,本质原因是模型能共用。
1.2 碳排放交易和需求响应为什么要同时考虑
传统经济调度只看发电成本,哪个机组便宜就让哪个多发,碳排多少不太关心。但现在研究趋势变了,碳排放是有价的,排多了要去市场买配额,排少了可以卖配额赚钱,这个价格信号会直接影响机组的调度顺序。把碳交易成本写进目标函数以后,燃气轮机这种相对清洁的机组可能要代替柴油机多发电,因为柴油机虽然燃料成本看着不高,但碳排放强度高,把碳成本一算进去综合代价可能反而更高。
需求响应是另一条降本路径。用户负荷不是铁板一块,有的洗衣机、热水器可以把用电时间从峰时挪到谷时,有的空调可以在高峰期降低一点功率换取补偿。这些可控负荷等于给调度员多了一组“旋钮”,可以在电价高、发电紧张时把负荷压下来,在夜里面电便宜时把负荷填上去。
把碳交易和需求响应放进同一个日前调度模型,本质上就是一个多资源协同优化问题:左边是各种分布式电源和外部电网作为供给端,右边是包含可调负荷和储能在内的广义需求端,中间再用一台“环境计价器”(碳市场)把高碳机组的代价显性化。这个框架能解释当前微网运行中的很多实际决策,这也是这个课题被大量论文采用的原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模型怎么搭,数学问题是关键
2.1 日运行目标成本到底有哪些项
先给出我通常采用的统一目标函数,基本能覆盖大多数需求:
[
\min \sum_{t=1}^{24}
\left[
C_{MT}(P_{MT,t}) + C_{DE}(P_{DE,t}) + c_{om}^{MT}P_{MT,t} + c_{om}^{DE}P_{DE,t}
\right.
]
[
\quad\quad + c_{buy,t}P_{buy,t} - c_{sell,t}P_{sell,t}
]
[
\quad\quad + C_{start,MT} \cdot u_{start,t} + c_{cut}P_{cut,t} + \lambda_{curt}P_{curt,t}
\left.
\right] + C_{carbon}(E_{total}, E_{quota})
]
其中:
- (C_{MT}) 和 (C_{DE}) 是燃气轮机、柴油发电机组的燃料成本,工程上常用二次函数 (aP^2+bP+c),在Matlab里需要处理为分段线性或直接交给MIQP求解器;
- (c_{om}) 是单位运维成本,一般很小,但体现“放电也要磨损电池”;
- (c_{buy,t}P_{buy,t}) 是向大网购电成本,(c_{sell,t}P_{sell,t}) 是向大网售电收益;
- (C_{start,MT}) 是机组启动成本,要不要加取决于你算例里有没有考虑启停状态;
- (c_{cut}P_{cut,t}) 是激励型需求响应补偿,用户同意削减负荷,你要按合同单价付钱;
- (\lambda_{curt}P_{curt,t}) 是弃风弃光惩罚项,光伏风电预测出力用不完时弃掉的代价,设得比购电价高一些,优化就会尽量少弃;
- (C_{carbon}) 是整个24小时碳排放交易成本函数,我会在第2.4节专门讲。
目标函数不唯一,比如有的文章还加了储能老化成本、可转移负荷的舒适度惩罚等,但核心骨架基本是这些。第一版模型建议先不要贪多,把电源成本、购售电、储能、DR、碳交易跑通,后面再往里面加各种花活。
2.2 需求响应要分类型建模,不能一锅炖
标题里“多种需求响应”怎么落地是我被问最多的问题。我看到很多复现代码特别粗暴,直接写一个 (P_{DR,t}),说这个就是需求响应后的负荷,然后约束 (0 \le P_{DR,t} \le P_{max}),这写出来基本等于什么都没约束,求解器想怎么拧就怎么拧。
更接近论文和工程实际的做法,是把DR分成三类,分别建模:
第一类是可转移负荷(shiftable load),它总用电量不变,只是用电时间可平移,典型如电动车充电、洗衣机、提前制冰的蓄冷空调。建模最简单清晰的方案是引入二维转移矩阵 (M_{i,j}),表示原计划在 (i) 时段开启、实际被挪到 (j) 时段的电量。约束条件包括:
[
\sum_{j} M_{i,j} = L_{shift,i}^{base}, \quad \forall i
]
保证每个时段的原始可转移电量都被安排出去,总量守恒。同时限制转移窗口,比如只能前后移3小时,(j \notin [i-3, i+3]) 时 (M_{i,j}=0)。调度后某时段实际承接的可转移负荷是:
[
L_{shift,j}^{after} = \sum_{i} M_{i,j}
]
也就是说,到了傍晚大家把洗衣机挪到凌晨,凌晨的负荷数就会增加,这部分增加的量正是白天转移过来的量。这个二维向量在处理凌晨/白天转移时很直观,而且转移窗口能防止“把下午的负荷挪到半夜两点”这种不合理结果。
第二类是可削减负荷(curtailable load),典型是空调温度上调、照明减半,总用电量确实减少了。建模最简单,给每个时段加一个削减上限,比如不超过该时段可削减基线的15%:
[
0 \le P_{cut,t} \le \alpha \cdot L_{cutable,t}^{base}
]
削减量进入功率平衡方程变成负号,同时目标函数加上补偿单价 (c_{cut})。注意补偿单价不要设太低,否则优化结果总是让用户无限削减。设太高则永远不削减,那需求响应模块就白加了。一般取该时段购电价的0.5到0.8倍左右比较合理,晚峰购电价1.2元时,削减补偿设在0.7元上下,削出来的电才算有性价比。
第三类是价格型需求响应(price-based DR)。严格来说,如果电价本身是外部已知的分时电价,用户会根据峰谷价差调整用电习惯,这属于“被动响应”。工程模型常用负荷价格弹性矩阵描述:
[
\frac{\Delta L_t}{L_t^{base}} = \varepsilon_t \cdot \frac{\pi_t - \pi_t^{base}}{\pi_t^{base}}
]
其中 (\pi_t) 是分时电价,(\pi_t^{base}) 是参考平价,(\varepsilon_t) 是自弹性系数,通常取负值。算下来每一时刻的负荷变化量其实是常数,可以直接平移进负荷基线里,不需要再加决策变量。如果你想做“电价作为决策变量反馈给用户响应”的闭环,那就涉及需求侧竞价,属于另一套市场模型,很多复现代码在这里没讲清楚,导致写出来的模型出现负载乘以价格的非线性项,劝你第一版别碰。
2.3 储能、联络线、功率平衡这些硬约束
储能是日前调度里最“艺术”的部分,因为时间耦合性很强。核心状态方程:
[
SOC_{t+1} = SOC_t + \left( \eta_{ch}P_{ch,t} - \frac{P_{dis,t}}{\eta_{dis}} \right) \frac{\Delta t}{E_{cap}}
]
SOC要约束在 ([SOC_{min}, SOC_{max}]),充放电功率分别在 ([0, P_{ch}^{max}]) 和 ([0, P_{dis}^{max}])。最关键的是要防止同一时刻既充电又放电,否则优化器为了凑平衡会干出“边充边放”刷损耗的蠢事。工程上有两种处理:加二进制变量 (u_{sto,t}),让 (P_{ch,t} \le u_{sto,t}P_{ch}^{max})、(P_{dis,t} \le (1-u_{sto,t})P_{dis}^{max});或者不用二进制,直接在约束里写 (P_{ch,t} \cdot P_{dis,t} = 0),但这是非线性约束,Yalmip里没法直接处理,所以正式模型基本都用带二进制变量的写法。
运行周期首尾SOC通常要求回到同一个值,保证储能状态在日循环内闭环:
[
SOC_1 = SOC_{25} = SOC_{init}
]
有了这个约束,早上8点充的电,傍晚才有机会放掉,不会出现一天结束电池还是空的、第二天无电可用的情况。
功率平衡是整个调度模型的中枢等式。给一个所有电源都在左侧、所有用电在右侧的汇总形式:
[
P_{MT,t} + P_{DE,t} + P_{wt,t} + P_{pv,t} + P_{dis,t} + P_{buy,t}
]
[
= L_{t}^{base} + L_{shift,t}^{after} - L_{shift,t}^{base} - P_{cut,t} + P_{ch,t} + P_{sell,t} + P_{curt,t}
]
注意风电光伏在等式左边直接用可用出力,如果容量大于需求,优化器会通过正 (P_{curt,t}) 在右边吸收掉一部分,表示弃风弃光;如果加了曲线惩罚,它就不会轻易弃。左边加电、右边减电,这个约定一定要统一,否则约束差个正负号,求解结果会让你怀疑人生,排查要花半天。联络线的购电 (P_{buy,t}) 和售电 (P_{sell,t}) 也需要限制上限,通常两者共同占据一条联络线容量,可加:
[
P_{buy,t} \le P_{line}^{max} \cdot u_{grid,t}, \quad
P_{sell,t} \le P_{line}^{max} \cdot (1-u_{grid,t})
]
2.4 阶梯碳交易成本怎么处理才能真正进入目标函数
碳排放交易建模最常出现在三种级别。最粗糙的是把碳价直接乘排放量,当作一个固定系数 (c_{CO2} E_{total}) 加入目标,但标题既然专门写“计及碳排放交易”,一般默认采用基准线法配额分配。
基准线法的核心是:先给发电机组一个免费碳排放配额,它和实际发电量挂钩:
[
E_{quota} = \sum_{t} \delta \cdot \left( P_{MT,t} + P_{DE,t} \right) \Delta t
]
(\delta) 表示单位发电量的免费配额强度,是外部输入参数。机组实际碳排放量按各自的排放因子算:
[
E_{total} = \sum_{t} \left( \mu_{MT}P_{MT,t} + \mu_{DE}P_{DE,t} \right) \Delta t
]
当 (E_{total} \le E_{quota}),机组配额有剩余,可以在碳市场卖出获利,目标函数中表现为负成本;当 (E_{total} > E_{quota}),需要购买缺口配额。
只做一档碳价是最简单的线性情况:(C_{carbon} = c_{buy} \cdot \max(E_{total}-E_{quota},0) - c_{sell}\cdot\max(E_{quota}-E_{total},0)),这个在Yalmip里可以用两个非负变量之差表示。
但如果要体现近几年论文常说的“阶梯碳价”,多出的购买量要按超额比例分段计价,超额越多价格越贵。此时先把净购买量拆成正负两部分:
[
E_{total} - E_{quota} = E_{buy} - E_{sell}, \quad E_{buy}\ge0,\ E_{sell}\ge0
]
再给 (E_{buy}) 分三段:第一段 (0\sim Q_1) 价格 (c_1),第二段 (Q_1\sim Q_2) 价格 (c_2),第三段 (Q_2\sim Q_3) 价格 (c_3),满足 (c_1<c_2<c_3)。因为价格递增,目标函数自动会优先填满低档,所以只需要引入三个连续非负变量 (e_1,e_2,e_3) 和档位上限约束,不需要额外二进制变量:
[
e_1 \le Q_1,\quad e_2 \le Q_2 - Q_1,\quad e_3 \le Q_3 - Q_2
]
[
E_{buy} = e_1 + e_2 + e_3
]
碳交易成本写为:
[
C_{carbon} = c_1 e_1 + c_2 e_2 + c_3 e_3 - c_{sell}E_{sell}
]
我的建议是第一版先跑通一档碳价,确认结果合理后再扩成三档阶梯。直接上阶梯碳价的最大问题是分段界限设置不合理导致优化器频繁让燃机停机,结果忽高忽低,很难判断是不是模型错了。
3. Matlab实现步骤与核心代码框架
3.1 算例参数怎么设计才真实
很多同学卡在第一步,因为网上找到的论文参数不全,只能自己编数。我用的是一套园区微网常见基准参数,分享给你参考:
| 对象 | 参数 | 取值 |
|---|---|---|
| 可平移负荷 | 占总负荷比例 | 15% |
| 可削减负荷 | 削减上限比例 | 该时段可削减基线的20% |
| 燃气轮机 | 容量 / 出力下限 | 0.3 MW / 0.03 MW |
| 储能 | 容量 / 最大充放功率 | 0.6 MWh / 0.12 MW |
| 储能初始SOC | 运行首尾SOC | 20% |
| 上级联络线 | 购售电功率上限 | 0.6 MW |
| 光伏/风电 | 预测最大值 | 0.15 MW / 0.1 MW |
| 购电价(峰/平/谷) | — | 1.2 / 0.8 / 0.45 元/kWh |
| 售电价 | — | 0.35 元/kWh |
| MT排放因子 | (\mu_{MT}) | 0.68 kg/kWh |
| DE排放因子 | (\mu_{DE}) | 0.92 kg/kWh |
| 免费配额强度 | (\delta) | 0.5 kg/kWh |
| 碳交易价 | (c_{buy} / c_{sell}) | 90 / 80 元/t(折0.09/0.08元/kg) |
电力系统里的价格差一两个数量级很正常,做仿真前先把单位统一成“元、kWh、kW、kg”。我之前有个学生直接把论文里的碳价以元/t写进目标函数,结果碳成本比购电成本大了几千倍,模型所有机组全停机只买碳配额,折腾了两天才发现是单位没换算。
这里还要注意:可转移负荷的比例不能拍脑袋随便设。最优的平移结果会受可平移容量约束。你可以先跑一版不含DR的调度,看峰谷差多大,再据此把可转移比例设为峰谷差的10%-20%,这样算出来DR的削峰填谷作用才一目了然。
3.2 Yalmip搭模型的核心代码框架
Matlab里用Yalmip搭这类调度问题非常方便,因为它天然支持sdpvar连续变量、binvar二进制变量,约束可以像矩阵拼接一样写。下面是我习惯的骨架,数据部分用占位符表示,核心思路都在。
matlab复制%% 1. 基础参数
T = 24; % 日前调度24时段
E_cap = 600; % 储能容量 kWh
SOC_init = 0.2;
P_ch_max = 120; P_dis_max = 120; % 最大充放功率 kW
eta_ch = 0.95; eta_dis = 0.95;
P_mt_max = 300; P_mt_min = 30; % 燃气轮机功率上下限 kW
ramp_mt = 80; % 爬坡速率 kW/h
P_line_max = 600;
%% 2. 决策变量定义
P_mt = sdpvar(1, T, 'full'); % 燃机出力
u_mt = binvar(1, T); % 燃机启停状态,1表示开机
P_ch = sdpvar(1, T, 'full'); % 储能充电功率
P_dis = sdpvar(1, T, 'full'); % 储能放电功率
u_sto = binvar(1, T); % 储能状态,1放0充
SOC = sdpvar(1, T+1, 'full'); % SOC轨迹
P_buy = sdpvar(1, T, 'full'); % 购电功率
P_sell = sdpvar(1, T, 'full'); % 售电功率
P_cut = sdpvar(1, T, 'full'); % 可削减负荷削减量
P_cur = sdpvar(1, T, 'full'); % 弃风弃光功率
%% 3. 约束拼接
C = [];
% 功率平衡:左电源右负荷
C = [C, P_mt + P_wt_base + P_pv_base + P_dis + P_buy ...
== L_base + L_shift_after - L_shift_base - P_cut + P_ch + P_sell + P_cur];
% 燃机出力与爬坡约束
C = [C, P_mt_min .* u_mt <= P_mt <= P_mt_max .* u_mt];
C = [C, -ramp_mt <= diff(P_mt) <= ramp_mt];
% 如果设置了启动成本,配合启停变量 u_mt 一起用
% 储能约束
C = [C, SOC(1) == SOC_init, SOC(T+1) == SOC_init];
C = [C, SOC_min <= SOC(2:end) <= SOC_max];
C = [C, SOC(2:end) == SOC(1:end-1) + ...
(eta_ch * P_ch - P_dis / eta_dis) * 1 / E_cap * 1000];
C = [C, 0 <= P_ch <= P_ch_max .* (1 - u_sto)];
C = [C, 0 <= P_dis <= P_dis_max .* u_sto];
% 联络线购售电限制
C = [C, 0 <= P_buy <= P_line_max .* u_grid];
C = [C, 0 <= P_sell <= P_line_max .* (1 - u_grid)];
% 可削减负荷容量约束
C = [C, 0 <= P_cut <= 0.2 * L_cutable_base];
% 可转移负荷:二维转移矩阵 + 守恒 + 窗口
M_var = sdpvar(T, T, 'full');
for i = 1:T
C = [C, sum(M_var(i,:)) == L_shift_base(i)]; % i时段原始量全部安排
for j = 1:T
if abs(j - i) > shift_window % 只能前后移shift_window小时
C = [C, M_var(i,j) == 0];
end
end
end
L_shift_after = sum(M_var, 1); % 各时段实际承接的可转移负荷
%% 4. 碳排放与碳成本
E_quota = sum(0.5 * (P_mt + P_de)) * 1; % 注意单位kg
E_real = sum(0.68 * P_mt + 0.92 * P_de) * 1;
E_buy = sdpvar(1, 1, 'full');
E_sell = sdpvar(1, 1, 'full');
C = [C, E_real - E_quota == E_buy - E_sell];
C = [C, E_buy >= 0, E_sell >= 0];
C_carbon = 0.09 * E_buy - 0.08 * E_sell; % 一档碳价示例
%% 5. 目标函数与求解
objective = sum(C_mt_coef .* P_mt .^2 + C_mt_linear .* P_mt ...
+ c_om_mt .* P_mt + c_om_de .* P_de ...
+ P_buy .* c_buy - P_sell .* c_sell ...
+ c_cut .* P_cut + 300 .* P_cur) + C_carbon;
optimize(C, objective, sdpsettings('solver','gurobi','verbose',2));
上面这个片段不能直接复制运行,因为负荷数据 (L_base)、光伏风电预测、可转移负荷基线、成本系数都要自己填。但变量定义和约束组织方式是按标准MILP来的,你把自己的数据套进去就行。
一个很容易被忽略的细节是储能SOC方程里的系数。SOC一般用0~1表示,充电功率单位是kW,乘以Δt=1小时得kWh,除以额定容量 (E_{cap})(kWh)才是SOC变化量。如果 (E_{cap}) 是600kWh而功率是120kW,一个时段充下来最大SOC增加0.2,初值20%,一个时段后顶多到40%,不要为了显示储能被频繁使用而把系数乘错,否则SOC会飞出上下界,要么infeasible要么结果非常反常。
3.3 结果提取与画图
求解完之后用 value(P_mt) 拿变量数值,再画图。我一般习惯按照下面这几个子图画在一个figure里:
- 第一张子图:各电源出力堆叠面积图,再加一条外购电曲线;
- 第二张子图:储能SOC曲线和充放电功率柱状图;
- 第三张子图:需求响应前原始负荷和DR后实际净负荷对比,旁边标出各时段削减量;
- 第四张子图:碳交易缺口/盈余柱状图,以及各时段机组碳排放量。
matlab复制P_mt_opt = value(P_mt);
P_ch_opt = value(P_ch);
P_dis_opt = value(P_dis);
P_buy_opt = value(P_buy);
L_dr = value(L_base + L_shift_after - L_shift_base - P_cut);
figure;
subplot(3,1,1);
bar([1:T], [P_mt_opt; P_pv_base; P_wt_base; P_dis_opt]', 'stacked'); hold on;
plot(1:T, L_dr, '-o', 'LineWidth', 1.5);
legend('MT','PV','WT','放电','净负荷');
xlabel('时段/h'); ylabel('功率/kW');
subplot(3,1,2);
stairs(0:T, value(SOC), 'LineWidth', 1.5);
xlabel('时段/h'); ylabel('SOC'); ylim([0,1]);
subplot(3,1,3);
bar(1:T, [P_ch_opt; -P_dis_opt; P_buy_opt]');
读图的经验是,第一眼看净负荷曲线是不是被削峰填谷了,第二眼看储能SOC是不是平稳走完一个日循环,第三眼看碳交易成本是不是压在可接受范围。如果净负荷在某个时段出现尖刺,大概率是数据里负荷、光伏和DR容量不匹配,回头检查该时段的功率平衡等式。
3.4 一个可预期的仿真结论长什么样
拿上面这套框架跑下来,典型的结论画像大概是这样的:
- 晚高峰时段购电价最高,燃气轮机出力会顶到上限,储能从下午开始提前蓄电,在晚高峰期放电;
- 可转移负荷明显从晚高峰挪到凌晨低谷,可削减负荷在傍晚被削减到上限附近;
- 随着碳价设置升高,燃气轮机相比柴油机的相对成本优势会增加,因为天然气机组排放强度低,碳交易支出少,于是MT优先调度,柴油机出力被压低;
- 需求响应参与后,系统总运行成本比不参与时下降8%左右,碳交易成本下降多少取决于配额基准线设置。
这些结论如果反向出现,比如高峰时段储能反而在充电,那就要倒回去检查是不是分时电价时段划分反了,或者电池效率太高导致峰谷套利不起作用。我见过最离谱的一次是储能SOC系数写反导致傍晚SOC计算值还能超过1,优化器硬是把充电放在晚高峰,以平衡那个错误的SOC方程,这种错误光看成本比很难发现,一定先把SOC曲线打出来检查。
4. 复现过程中最容易踩的坑
4.1 求解器报infeasible,八成不是数据问题
跑通之前,主流的红色报错是 Infeasible problem。我最初也怀疑数据边界不对,但排查后总结出三个高频原因:
第一,启停约束和爬坡约束打架。比如你允许燃机在某一时段从停机直接跳到大出力,但爬坡约束同时限制了相邻时段的变化量,开机时段 (diff(P_{mt})) 跨度巨大,超过爬坡限制,模型无解。正确做法是给燃气轮机加减一个带松弛变量的机组组合约束,或者把爬坡约束改成:
[
-R \le P_{mt,t} - P_{mt,t-1} \le R
]
再额外用 (P_{mt,t} \le P_{mt,max}u_{mt,t})、(P_{mt,t} \ge P_{mt,min}u_{mt,t}) 控制启停。如果启停和爬坡同时要严格约束,需要引入启动/停机二进制变量的“三态约束”来衔接,这也是很多基础教程省略的部分。
第二,储能首尾SOC约束和充放电功率上限冲突。如果SOC初值等于下限0.1,又要求一天结束回到初值,而储能最大容量只能容纳从傍晚充起来的电量,很可能一小时段内就把电量放完或充不进去,整体无解。排除方法很简单:把首尾SOC约束的初始值先放宽到 (SOC_1=SOC_{25}) 且都在0.2-0.8之间,如果还是无解再看别的约束。
第三,负荷平衡等式和整数变量组合导致求解器空位。最常见的是可转移负荷二维矩阵规模太大,转移窗口设置错误间接产生矛盾约束。你可以在约束里逐个打印约束残差:
matlab复制check(C)
Yalmip会输出每个约束的残差量,残差超级大的那两条约束就是问题源头,比盲猜省时间得多。
4.2 需求响应老是“过度响应”怎么办
我实测算例时发现,如果只给可转移负荷加“总量守恒+窗口限制”,优化器会把所有负荷尽可能挪到电价最低的午间或后半夜,导致该时段负荷出现新峰值,虽然运行成本降了,却违背用户实际用电习惯。
解决办法有两个方向。第一个方向是给转移行为增加一个激励成本,比如用户不喜欢把负荷挪得离原时段太远,转移距离越远成本越高:
[
C_{shift} = \sum_{i,j} \gamma \cdot |i-j| \cdot M_{i,j}
]
这个惩罚项会让优化器优先选择相邻时段平移,而不是一脚把负荷从晚8点踢到凌晨4点。第二个方向是把转移功率本身也加一个上限,防止某个时段塞入过多转移负荷形成新峰:
[
\sum_i M_{i,j} \le L_{shift}^{max}, \quad \forall j
]
两个约束配合使用更贴近工程实际。加了以后,你可以对比不加的情况,会看到负荷曲线比较平滑,而不是每个低谷时段都“鼓起一个包”,这也让削峰填谷效果更好看。
4.3 碳交易模块算出来的碳排放量不对劲
每次写碳交易模块,我都要提醒自己检查配额的核算口径。配额到底按什么发电量发?有些论文把配额发给所有电源发电量,包括光伏和风电;有些只发给化石能源机组。这两种做法会产生两个方向相反的调度结果:
- 按发电量 (\sum(P_{MT}+P_{DE}+P_{PV}+P_{WT})) 配配额,新能源出力越多,整系统免费配额越多,碳交易成本越容易被压低;
- 只按化石能源 (\sum(P_{MT}+P_{DE})) 配配额,因为实际排放也来自化石能源机组,这样“配额与排放”口径一致,更符合基准线法在发电行业的做法。
我常用的是第二种。同时注意配额强度 (\delta) 如果设得高于机组的实际排放因子,机组永远不需要买碳,碳交易机制不起任何约束作用;反之如果设得非常低,所有化石机组都要大额购买配额,碳成本会主导调度结果,燃气轮机也可能不敢开,导致供电不足或者转向网购电。工程上合理的取值是让配额强度略低于平均排放因子,这样部分机组需要买碳,部分机组还能卖碳排放权获利,模型才有博弈空间。
另外,目标函数里若同时有买碳和卖碳,必须像前面那样保证 (E_{total}-E_{quota}=E_{buy}-E_{sell})。很多人直接写成 (C_{carbon} = c(E_{total}-E_{quota})),这样当排放低于配额时,碳成本变成负数,逻辑上倒也没错;但只要一引入购电的间接排放,符号就很容易写反,我建议始终用 (E_{buy}) 和 (E_{sell}) 两个非负变量显式区分,排查和改代码都方便。
4.4 求解器选型直接影响体验
一般Matlab自带的 intlinprog 可以解这类MILP,但变量一多、时段一细,求解速度会明显下降。虚拟电厂如果做96个时段、含多台机组和多个DR资源,用 intlinprog 可能会卡到分钟级。我推荐装Gurobi或CPLEX,Yalmip支持极好,直接:
matlab复制sdpsettings('solver','gurobi')
能比默认求解器快一个数量级。如果实在只有Yalmip没有外部求解器,也可以先用 sedumi 解LP松弛,看看非整数解是否合理,再用 intlinprog 做最终验证,这招在初步调试时特别好用。
5. 怎么往更深的方向扩展
5.1 从确定性日前走向不确定性处理
前面所有模型都假定光伏、风电、负荷预测值是精确的,这也是“日前”调度的天然局限。实际中光伏预测误差很容易超过20%,如果调度结果恰好把这种高估的光伏出清掉,到第二天实时运行时就会出现功率缺额。如果你要把模型写成小论文,往上走的第一步一般是加不确定性处理。
常用做法有三类:随机规划是给预测误差设几个典型场景,每个场景配一个概率,让目标函数变成期望值;鲁棒优化是构建最坏场景区间,让调度结果在最恶劣出力组合下也不越限;分布鲁棒则是结合前两者的思路。落到代码上,随机规划只比你现在的模型多一个场景维度循环,相当于把原来每个时段变量复制 (N) 份,目标函数变成概率加权求和:
[
\sum_s \rho_s \cdot f(x_s) + \text{first-stage cost}
]
这里第一阶段成本是储能、机组开停和购电合同的日前决策,第二阶段是各场景下的调整代价。改造之前,先确认你基础模型的求解时间能接受,否则扩展到96时段加20个场景后,两阶段变量会膨胀到很大。
5.2 DR从用户侧“被动”参与变成主动决策
另一个常见扩展方向是把单一的价格型DR升级成用户效用最大化模型。你可以给负荷端加一个满意度函数或效用函数,让用户在“多用电获得的效用”和“电费支出”之间做权衡,调度的目标就不再是最小化运营成本,而是最大化社会福利。这样需求响应的结果不再是硬性削减,而是市场均衡下的自然选择,文章档次会高不少。
我自己做过的一个改进是引入了可转移负荷的“用户温度舒适度惩罚”,用空调的等效热参数模型(ETP)把室内温度作为状态变量,用户在高峰时段允许空调升1度,但温度偏离设定值越多,舒适度惩罚越大。这种模型的物理含义很清晰,画出来的结果也容易解释,适合作为VPP文章的亮点。
扩展的同时不要忽视调用的稳定性。每增加一类需求响应,目标函数和约束都要重新做一次线性化审查。我强烈推荐每次改动后都跑一次无DR的标准算例,和带DR的结果做差,确认边际成本变化方向合理,再继续加复杂度。不要在模型还没跑通时就堆叠几十个约束,那样报错以后完全不知道错在哪一层。
另一个实用技巧是把调度结果输出成Excel或Matlab结构体保存,方便回头复现。我平时会统一存 P_mt_opt, P_ch_opt, SOC_opt, C_carbon_total 这些关键字段,在做不同碳价参数扫描时很有用。改一个配额强度,重新求解,然后再对比几张曲线,比每次在命令窗口反复找变量强得多。
