先交代一句:这个项目是我最近整理微网/虚拟电厂这块代码时重点打磨的一个场景。标题里的几个关键词其实已经把核心讲透了——微网/虚拟电厂,日前优化调度,碳排放交易,多种需求响应,Matlab实现。现在做这类问题的人不少,但我发现很多人的模型要么只做了碳交易没做需求响应,要么把需求响应简单当成一个固定比例削峰,一深入就露馅。这篇文章把我实际建模和写代码过程中整理出来的思路完整铺开,包括模型怎么拆、目标函数怎么列、约束怎么加、碳成本怎么处理、需求响应怎么分类、代码调试容易栽在哪里,都会讲到。适合正在做相关课题的研究生、写论文需要算例的,以及想从传统经济调度往低碳调度方向转的工程师参考。
1. 为什么要做“碳交易+需求响应”的日前优化调度
1.1 微网调度的本质:提前给未来24小时排一张可靠的运行时刻表
微网/虚拟电厂的日前优化调度,说白了就是在前一天,根据负荷预测、新能源出力预测、分时电价这些信息,把未来24小时里每一台分布式电源的出力和启停、储能的充放电功率、和上级电网的买卖电功率全部定下来。目标是让整个系统在满足用户用电需求的前提下,运行成本最低或者综合效益最高。
这套逻辑听起来不复杂,但真正落地的难点在于:约束条件非常多。燃气轮机有出力上下限和爬坡速率限制,储能不能同时充放电而且SOC要保持在合理区间,买电和卖电不能同时发生,功率平衡每时每刻都得成立。更麻烦的是,这些约束不是独立存在的,它们通过功率平衡方程耦合在一起,牵一发而动全身。
可以把它理解成排班问题:你有值班人员、设备、库存,需要在各种限制条件下排出一天最优的班表。运行调度只是把“人员”换成了“机组和储能”,把“库存”换成了“SOC状态”,本质是一个带约束的优化问题。
1.2 碳交易加入后,问题从“省钱”变成了“省碳钱”
传统经济调度只关心购电成本、燃料成本这些直接费用。但碳排放交易机制引入后,系统中每一吨二氧化碳排放都有了价格。如果微网内的实际碳排放量超过免费配额,就需要去碳市场购买配额,这部分费用直接进入运行成本;如果排放量低于配额,多余的配额反而可以出售变成收益。
这就改变了机组的调度优先级。比如一台燃气轮机的发电成本可能比外网购电便宜,但如果它的碳排放因子较高,加上碳成本之后,综合成本可能反而更贵。于是调度结果就会从“多用气电”变成“多买网电”,或者让储能更积极地在高排放时段放电。
所以在代码里不能只加一个简单的“碳排总量约束”就完事,那样只是一个硬限制,体现不了“交易”的含义。碳交易的特点在于:配额是可以买卖的,它通过价格信号引导调度决策,而不是一刀切。
1.3 需求响应让负荷从“固定值”变成了“可调节资源”
传统调度里,负荷曲线是给定不变的输入,调度只能被动适应。但实际用户是有调节潜力的——高峰时段让一部分负荷削减或者转移到低谷时段,不仅能缓解供电压力,还能降低系统整体运行成本。
需求响应不是一种单一资源。电价敏感的用户会主动调整用电行为,这是价格型需求响应;签订协议的用户按照调度指令削减或转移负荷并获得补偿,这是激励型需求响应。这两类响应的时间尺度和对象完全不同,在建模时必须分开处理,一股脑塞进一个公式里很容易出错。
把需求响应资源和碳交易一起纳入日前调度,系统就同时具备了“电源侧调节”“储能侧搬运”“负荷侧配合”三种灵活性,这样得到的调度方案才更接近实际运行场景。
2. 模型怎么搭:目标函数、约束和决策变量
2.1 先用一个清晰的优化问题框架把任务定下来
不管代码写得多么花哨,底层一定是一个标准优化模型:
- 决策变量:需要求出来的量,比如各时段燃气轮机出力、储能充放电功率、购售电功率、负荷削减量、可平移负荷的启动状态。
- 目标函数:需要最小化或最大化的量,这里就是系统总运行成本。
- 约束条件:决策变量必须满足的物理限制和运行限制。
我用的是单目标最小化,把碳交易成本和需求响应补偿成本全部折算成经济量放进目标函数。这样做的好处是模型直观,求解稳定,而且方便做碳价、补偿价格的灵敏度分析。如果你想做成多目标,比如“成本最小+碳排放最小”,也可以,但要注意两个目标的量纲和权重选取,处理起来会复杂不少。
2.2 目标函数里有哪些成本项,它们分别代表什么
我这个模型的目标函数主要包括六块:
- 与上级电网交互成本:向电网买电的费用减去向电网售电的收益,分时电价下这部分是调度决策的重要驱动力。
- 燃气轮机的燃料成本和启停成本:燃料成本通常用出力的二次函数或分段线性函数表示,启停成本在日内多次启停时不可忽略。
- 储能运行维护成本:充放电会造成电池循环损耗,一般按充放电功率乘以一个较小的单位成本系数来计。
- 碳排放交易成本:实际碳排放量与免费配额之差对应的费用,阶梯碳价下是一组分段线性成本。
- 需求响应补偿成本:可削减、可转移、可平移负荷参与调度后需要支付给用户的补偿费用。
- 弃风弃光惩罚成本:新能源出力没有被完全消纳时的惩罚项,用来避免模型为了省钱而随意丢弃清洁能源。
这六项加起来就是一个完整的日前运行成本。写代码的时候我习惯把每一项单独算出来存成变量,最后在目标函数里直接相加,这样做完优化后可以单独查看每一项的成本构成,对分析调度结果非常有帮助。
2.3 约束条件:功率平衡、机组、储能、碳排放、需求响应一个都不能少
约束条件可以根据对象分成几组:
功率平衡约束是核心,它要求任何时刻系统内的发电加上购电,等于负荷加上售电和充电。这里要注意,负荷侧用的是“参与需求响应之后的实际负荷”,所以在约束里要把削减量减掉、把转移负荷加上,细节很容易漏。
机组约束包括出力上下限、爬坡速率约束、最小启停时间约束。储能约束包括SOC递推关系、充放电上下限、同一时段不能同时充放电的互补约束。购售电约束包括联络线功率上限和买卖互斥,不能让模型在同一时段又买又卖来套利。
碳排放约束在这类问题里往往不是硬约束,而是通过碳交易成本在目标函数中体现。但如果配额分配方式或者阶梯碳价设置得很宽松,模型几乎感受不到碳压力,这时候就需要单独检查碳排放总量,再决定要不要加约束。
3. 碳排放交易机制怎么落到目标函数里
3.1 从配额分配到实际排放,先把流程走通
要在代码里实现碳交易成本,首先要搞清楚三个量:免费配额是多少、实际排放是多少、二者差值对应多少成本。
微网碳排放源有两个:一是本地燃气轮机发电产生的直接排放,二是从上级电网购电间接产生的排放,因为网电在上游火电厂发电时同样会排放二氧化碳。这一点很多人会漏掉,漏掉的后果是模型会倾向于无限外购电来逃避本地排放,导致调度方案失真。
免费配额的处理我通常采用按负荷占比或装机容量的简化方式:配额总量等于某一系数乘以预测总负荷,再分配到24小时。这样处理不是为了精确模拟碳市场规则,而是为了让模型抓住碳交易的本质:配额以内不花钱,超额要买,少排可以卖。
3.2 阶梯碳价的线性化处理:为什么不能直接写一个分段判断
实际碳交易机制中碳价往往不是固定的。一种常见的方式是阶梯碳价,即碳排放量超出配额的部分,按照超额量落在不同区间采用递增的单价。这样做比单一碳价更能反映减排压力递增的效果。
初写代码的人很容易想当然:在目标函数里用if判断超额量落在哪个区间,然后直接乘对应单价。这种思路在C语言或者普通脚本里没问题,但在优化模型里是行不通的——基于变量的if判断会引入非光滑特性,求解器根本没法处理。
正确的做法是把碳排放量拆成几个顺序区间,用额外的连续变量分别表示落在每个阶梯内的排放量,再用递增碳价对应的成本系数乘上去。核心逻辑是:既然碳价逐级递增,最小化成本的优化过程会自动优先填满低价区间,再往高价区间走,所以不需要引入额外的0-1变量来强制排序。
为了直观,我给出一个三段阶梯碳价的成本计算示意:
% E_total: 总排放量, E_quota: 免费配额 % 超出配额部分按三个阶梯计费 E_excess = E_total - E_quota; % 拆分成三段 E_level1 = sdpvar(1); % 第一段: <= limit1 E_level2 = sdpvar(1); % 第二段: limit1 ~ limit2 E_level3 = sdpvar(1); % 第三段: limit2 ~ 超过部分 Constraints = [Constraints, E_excess == E_level1 + E_level2 + E_level3]; Constraints = [Constraints, 0 <= E_level1 <= limit1]; Constraints = [Constraints, 0 <= E_level2 <= limit2 - limit1]; Constraints = [Constraints, 0 <= E_level3]; % 阶梯碳价递增, 优化会自动先填满低价区间 C_carbon = lambda1 * E_level1 + lambda2 * E_level2 + lambda3 * E_level3;如果你只想做一个简单版本,用单一碳价直接乘以超出配额部分也可以跑通。但阶梯碳价的结论会更丰富,特别是做碳价灵敏度分析时,能看出不同碳价水平下调度策略的拐点。
3.3 碳成本进目标函数后的连锁反应
碳成本进入目标函数之后,会发生一个很有意思的连锁反应:当碳价较低时,燃气轮机照常出力,因为碳成本占比太小;随着碳价升高,燃气轮机的综合发电成本逐渐超过外购电和储能放电的成本,调度策略开始转变;碳价极高时,系统甚至会牺牲部分经济性来压低碳排放总量。
这在实际碳市场中对应的含义是:碳价只有高到一定程度,才会真正改变电源结构的运行顺序。我在代码里预留了碳价扫描接口,可以一次性跑一组不同碳价下的调度结果,画出一条“碳价-总碳排放量”曲线。这条曲线通常存在一个明显的拐点,对分析碳交易机制的有效性非常有说服力。
另外要提醒一句:阶梯碳价写进目标函数后,模型性质仍然是线性的,没有二次项也没有整数变量的情况下用LP求解器即可;一旦需求响应部分加入可平移负荷的0-1变量,模型就变成了MILP,需要切换到支持整数规划的求解器。
4. 多种需求响应怎么建模:从经济概念到代码约束
4.1 价格型需求响应:用弹性矩阵描述用户对电价的反应
价格型需求响应不直接出现在调度约束里,它改变的是负荷基线。基本原理是:电价变化会引导用户改变用电行为,电价高的时候少用,电价低的时候多用。这种响应程度用需求价格弹性系数来描述。
一个常用的数学模型是用弹性矩阵把各时段负荷变化率和电价变化率联系起来。矩阵对角线元素是自弹性系数,通常为负值,表示本时段电价上升导致本时段负荷下降;非对角线元素是交叉弹性系数,通常为正值,表示某时段电价上升导致相邻时段负荷上升,因为用户把部分用电转移到了其他时段。
代码实现时,可以先算出一个“响应后的负荷曲线”,再把它作为调度模型的输入。这一步发生在优化之前,所以价格型需求响应本质上是改变了调度面对的需求背景。需要注意的是,弹性矩阵系数的大小直接决定了负荷调整幅度,取值太大会出现负荷曲线严重变形的现象,取值太小则响应效果微弱,一般需要根据实际用户聚合数据调试。
4.2 激励型需求响应:可削减、可转移、可平移要分开处理
激励型需求响应是真正作为决策变量进模型的,也是代码里最容易写乱的部分。我把它们分成三类:
可削减负荷是最简单的。用户允许调度方在特定时段削减一部分负荷,代价是按削减电量支付补偿。约束上只需要限制每个时段削减量不超过该时段可削减容量的上限,目标函数里加上削减补偿成本即可。
可转移负荷的复杂程度高一些。它要求转移前后总用电量保持不变,也就是说高峰时段减少的用电量必须转移到其他时段去,不能让总用电量凭空消失。建模时需要引入转移变量,并保证一天内转移负荷的总电量为零,目标函数里一般只计转移行为产生的操作成本或补偿成本。
可平移负荷是最麻烦的一类。它代表的是洗衣机、烘干机这类一旦启动就必须连续运行若干小时的负荷,不能拆散转移,只能把整条负荷块从一个时段搬到另一个时段。这类负荷必须用0-1整数变量来表示平移后的启动时刻,同时还要保证一个块在24小时内只能被启动一次。
三类负荷的组合让模型成为一个典型的混合整数线性规划问题。变量数量不算吓人,但逻辑关系必须梳理清楚,每个时段参与平衡的实际负荷是多少,要对照着公式一项项核对。
4.3 需求响应成本给不到位,模型会“疯狂削负荷”
需求响应建模中最典型的错误,是只加了削减负荷的约束,却没在目标函数里给足补偿成本。这样做的后果是:对优化求解器来说,削减负荷相当于减少用电需求,又不要额外付钱,它会毫不犹豫地把所有可削减负荷全部砍掉,得到一堆毫无实际意义的“削峰”结果。
正确做法是给每一类需求响应设置合理的单位补偿价格。可削减负荷的补偿价格一般参考售电电价的一定比例,可转移负荷的补偿要考虑用户调整用电习惯的不便程度,可平移负荷因为涉及人工操作或者设备预约,补偿标准通常更高。
还有一个容易忽略的点:可转移负荷转移后不能造成新的峰谷倒置。有些模型跑出来,低谷时段原本闲置的变压器在深夜被大量转移负荷塞满,这是不合理的。需要给每个时段的转移后负荷设一个上限,或者限制相邻时段的负荷变化率,避免模型为了省几个小钱制造出新的运行风险。
5. Matlab代码实现:从数据到结果的完整流程
5.1 代码模块怎么划分才方便调试
我写这类代码有一个原则:不要把所有逻辑塞进一个几百行的脚本里。哪怕只是学术验证,我也建议按模块组织。实际项目里我的文件结构大致是这样:
- 数据输入模块:定义24时段的基础数据,包括负荷预测、新能源出力、分时电价、燃气轮机参数、储能参数、碳配额参数、需求响应容量及价格。
- 变量定义模块:用sdpvar和binvar定义所有决策变量,并为每个变量写好注释,说明它代表什么。
- 约束生成模块:按照上文说的几组约束逐条添加,每加一组就加一行注释。
- 目标函数模块:把各项成本拆开计算,方便后续单独分析。
- 求解模块:设置求解器参数并调用optimize。
- 结果后处理模块:提取变量值,计算核心指标,画图。
看起来模块多了几步,但调试的时候优势非常明显。出了问题你可以直接定位是哪个约束组导致的不可行,而不需要从头到尾翻找。
5.2 用Yalmip建模时的核心变量写法和约束写法
Yalmip是我比较推荐的Matlab建模工具箱,它把变量定义、约束添加、目标函数书写和求解器调用都统一了,代码可读性高。核心变量定义大致是这个风格:
%% 决策变量 % 燃气轮机出力 P_mt = sdpvar(1, 24, 'full'); % 储能充放电功率(正值充电,负值放电可拆成两个变量) P_ch = sdpvar(1, 24, 'full'); P_dis = sdpvar(1, 24, 'full'); % 储能SOC SOC = sdpvar(1, 25, 'full'); % 购售电功率 P_buy = sdpvar(1, 24, 'full'); P_sell = sdpvar(1, 24, 'full'); % 可削减负荷量 P_curt = sdpvar(1, 24, 'full'); % 可平移负荷启动状态(0-1变量) u_shift = binvar(1, 24, 'full');功率平衡约束是最容易写错的一行。我一般这样写:所有电源出力加上购电和储能放电,等于固定负荷加上充电和售电,同时还要扣除削减负荷、加上转移过来的负荷。这个等式里的每一项都必须严格对应上数据,调试的时候可以先跑一个不包含需求响应的版本,让等式左右两边平衡后再加入需求响应项。
5.3 求解器配置和结果后处理的几个实用技巧
求解器方面,我优先选Gurobi或CPLEX,这两个对MILP的支持非常成熟。学术许可证可以免费申请,对大多数论文算例来说完全够用。配置代码一般是:
ops = sdpsettings('solver', 'gurobi', 'verbose', 2, 'debug', 1); ops.gurobi.MIPGap = 0.01; % 设置1%的MIP间隙 optimize(Constraints, Objective, ops);MIPGap的设置很关键。如果不设,求解器为了证明最优性可能会花很长时间;设成0.01表示允许1%的差距,对日前调度这种工程问题来说精度完全足够,求解速度可以快很多。
结果后处理我习惯输出几组核心数据:各时段电源出力和购售电组成的功率平衡表、储能SOC曲线、需求响应前后负荷曲线对比、碳交易成本及各成本项占比。画图时用Matlab自带绘图功能就够,重点是把功率平衡堆叠图、SOC曲线图、需求响应效果对比图这三张画清楚,论文里基本能直接用。
6. 调试实录:求解失败和结果不合理的几个典型案例
6.1 求解器报infeasible时,先怀疑数据不一致
很多初写者一看到infeasible就懵了,其实这个报错大部分不是代码逻辑错,而是数据本身不一致。我遇到最典型的情况是:负荷数据已经用了需求响应后的曲线,另一边又把削减负荷、转移负荷变量加进了功率平衡约束,等于同一份需求被扣减了两次,导致等式永远无法满足。
另一种常见数据冲突是储能SOC初值和末值设置不合理。如果要求一天结束时SOC回到初始值,但初始SOC设置过低、储能容量又不够在夜间完成充电,模型同样会不可行。调试时的建议很简单:把约束分成几组,每次只激活一组跑一遍,很快就能定位到底是哪一组约束把可行域锁死了。
6.2 Unbounded objective多数是漏了成本项或上限
目标函数无界这个报错,在碳交易模型里尤其容易踩。原因往往是碳配额免费量给得偏大,配方出售部分又没有任何约束,模型发现“多发点电然后卖配额”比正常卖电还赚钱,就会无限提高出力,目标函数一路向负无穷跑。
解决办法有两个方向:一是给燃气轮机出力设好上限,这本来就是物理约束,一般不会漏;二是给碳配额出售量设一个上限,模拟碳市场对交易量的实际限制。我自己一般两个都加上,既符合物理实际,又能让模型稳定收敛。
从数学上看,配方出售收益是目标函数中的负项,如果负项的系数绝对值大于发电收益项的系数,优化就会趋向无穷解。及时检查这一点可以省下大量排查时间。
6.3 负荷被切碎和储能频繁启停的“伪最优”问题
如果模型结果出现可削减负荷在不同时段以极其零碎的小数值出现,这虽然不违反约束,实际中却根本没法执行。因为没有用户愿意配合一个只削减0.01kW的调度指令,这种操作指令没有实用性。
解决方法是给可削减负荷加最小削减量和最小连续削减时长约束,让每一条削减指令达到用户能响应的门槛。储能端也有类似问题,如果不加最小充放电时间约束,求解器会给出频繁切换充放状态的锯齿形曲线,虽然数学上最优,工程上却会急剧缩短电池寿命。
我建议在写优化问题时就加入“储能不能连续在两个时段充电/放电切换过多次”这种限制,或者事后对锯齿曲线做一次平滑处理。从我的经验来看,在模型层面加约束比在结果后处理时修数据更可靠,因为后处理强行平滑会导致功率不平衡。
6.4 别忘了单位和步长:功率和能量是最容易混的两件事
这是最基础但最容易犯的错误。如果调度时段间隔是1小时,功率MW乘以1小时就是能量MWh,数值上正好相等,写起来确实方便。但一旦把时间尺度改成15分钟,就必须在能量递推公式里乘0.25,否则SOC轨迹会完全失真。
我的习惯是所有和储能能量有关的约束统一用MWh作为单位,功率约束统一用MW,在SOC递推和功率平衡两个关键位置反复确认单位是否一致。这个错误往往不会直接报错,而是表现为SOC曲线漂移、功率不平衡等看似“随机”的异常,排查起来比infeasible还费时间。
7. 这套模型还可以怎么扩展
这个模型的框架其实是开放性的,做好基础版本后可以往多个方向扩展。
如果从确定性走向不确定性,可以把风光预测出力从固定序列改为多场景,把日前调度扩展成两阶段随机优化或分布鲁棒优化,研究风光不确定性和碳价的联合影响。这个方向是目前论文里比较常见的前沿切入。如果研究重心在需求响应侧,可以把弹性系数从固定常数改成随时间变化的动态参数,或者把激励型需求响应升级为考虑用户满意度损失的非线性效用函数。
还有一个很实用的方向是参数灵敏度分析。代码本来就能跑通,直接把碳价、需求响应补偿价格、配额分配系数设置成循环变量,跑批看结果如何变化,可以高效得到不同机制参数对系统成本和碳排放的影响规律。
我个人在实际调试中的体会是:这类模型真正难的不是目标函数怎么写,而是每个决策变量之间的时序耦合关系。比如可平移负荷的持续状态需要往前回溯好几小时,储能SOC需要一直从初始时刻向后推,碳成本要分段拆解,这些都需要非常明确地从时段维度去理解。遇到不合理结果时,先在草稿纸上把某一时刻的功率平衡手工算一遍,大多数情况下都比盯着代码更先找到问题。最后再分享一个小习惯:每次跑完优化,我都会顺手把各成本项单独存出来看一眼,确认各项占比在合理范围里。如果碳交易成本突然变成绝对值巨大的负数,或者需求响应补偿成本高到离谱,那基本不是求解器的问题,而是前面哪一项收益或成本被重复计算了。这个习惯帮我拦下了不少让结果看起来“聪明到失真”的隐藏bug。