
搞这个项目的人应该不止我一个手里拿到一个微网/虚拟电厂的日前优化调度问题要求必须把碳排放交易和多种需求响应同时放进去还要给出Matlab代码实现。我最早接手时觉得这活儿挺简单——以前的调度模型谁没写过无非储能、光伏、风电、燃气轮机这几样目标函数加一项碳成本约束里加几个需求响应变量跑一个混合整数线性规划就收工。真做起来才发现碳交易和需求响应在一起不是多两个约束那么简单它们会互相牵制碳价高了燃气轮机就不敢多发电那负荷缺口谁来补需求响应要是把高峰负荷压得太狠储能的充放电策略又得跟着变。整个问题本质上变成了一个多目标、多时间尺度耦合、还带一堆0-1变量的组合优化问题。这篇文章我完整走了一遍从建模、编码、求解到结果分析的全过程包括最后调试时踩进去的几个坑。如果你正在写类似的“计及碳排放交易及多种需求响应的微网/虚拟电厂日前优化调度”代码或者准备用Matlab跑这类模型建议按我的思路先理清逻辑再动手写代码能省不少回头改模型的时间。1. 碳交易和需求响应为什么必须放进同一个优化模型先想清楚一个问题传统的经济调度到底缺了什么。传统日前调度只追求系统运行成本最小燃气轮机发电只看燃料成本储能充放电只看峰谷价差风电光伏用预测值负荷是不可调的刚性需求。在这个框架里碳成本和用户侧灵活性完全没有参与决策的位置。但实际运行中一个微网或虚拟电厂要面对的是配电网上游给的购电电价是分时的气价就是实打实的燃料成本碳配额有上限超排就得去碳市场买配额用户侧有大量可平移、可中断的负荷资源。这些要素每一个都直接影响机组的出力和购电计划不在同一个优化模型里考虑算出来的调度方案要么碳排放超标要么高峰时段被迫高价购电要么用户侧资源白白闲置。从建模角度看碳交易是通过“碳价信号”影响发电侧的边际成本燃气轮机每发一度电都有对应的碳排放因子碳配额不够时额外排放就要按碳价购买这个成本会进目标函数从而改变机组出力的经济排序。需求响应则是通过“弹性信号”影响负荷侧的形状把高峰时段可中断的负荷砍掉或者把可转移负荷挪到风电光伏出力大的时段负荷曲线变了整个系统的功率平衡点就变了最终导致机组出力、储能充放电、联络线交换功率全部重新分配。这两者必须放在同一个优化模型里协同求解原因在于它们的作用渠道是耦合的。举个例子碳价很高时燃气轮机出力被压低系统缺电这时候如果需求响应能把高峰负荷降下来就可以减小对外购电的依赖也避免了启动高成本、高排放的备用机组。反过来如果只做需求响应不考虑碳交易调度可能倾向于多用燃气轮机满足转移后的负荷碳排放反而升上去。只有同时建模才能在“经济性、低碳性、用户侧调节能力”三者之间找到一个帕累托最优的解。微网和虚拟电厂在这个问题上本质相同。微网有清晰的物理边界虚拟电厂则是把分散的分布式电源、储能、可控负荷聚合成一个整体参与调度但落到日前优化调度模型里都是把各种资源抽象成可调度单元连接点变成与外部电网的联络线功率。所以下面的模型对两者通用只是参数设定有所不同。2. 日前优化调度的数学模型目标函数和约束怎么搭2.1 优化变量与模型框架整个模型以1小时为调度时段一共24个时段。决策变量按资源类型分为几组燃气轮机各时段出力、启停状态、启停动作储能各时段充放电功率、充放电状态联络线各时段购电/售电功率或者直接用一个带正负的变量表示交换功率需求响应侧可转移负荷在各时段的实际用电量变化量、可中断负荷在各时段的中断状态、价格型需求响应后的负荷变化量碳交易相关各时段的实际碳排放量、碳配额购买量或出售量取决于模型采用基准线法还是总量配额法风电和光伏在这个模型里通常按预测值处理不作为决策变量但可以加弃风弃光变量在极端情况下允许削减出力并设置较大的惩罚系数。这样做的好处是让模型在负荷低谷或储能充满时不会因为“必须消纳全部新能源”而陷入无解。2.2 目标函数四项成本的博弈目标函数是整个模型的灵魂。我把总成本拆成四个部分写出来就是min F F_fuel F_startup F_grid F_carbon F_dr F_curtail逐项说明。燃料成本燃气轮机出力是二次凸函数一般用分段线性化处理。简单起见也可以用线性成本系数乘以出力。启停成本单独计启动一次有固定费用停机也消耗一次成本。购电成本联络线从主网购电分时电价是给定的输入购电功率乘以对应时段电价就是购电成本如果允许向电网售电再减去售电收益。这里有一个需要注意的细节同一个时段最好不要既购电又售电否则模型会因为价差出现套利而给出不合理的物理方案。处理办法是加一个购电状态0-1变量购电和售电功率互斥。碳排放交易成本这是题目里最核心的一块。常见做法有两种。第一种是基准线法。先根据机组的发电量乘一个基准排放强度算出免费配额。实际碳排放量超出配额的部分需要按碳价购买低于配额的部分可以按碳价出售相当于负成本。第二钟是阶梯碳价也就是把超排量划分成几个区间不同区间适用不同的碳价超排越多边际碳价越高。阶梯碳价更贴近实际碳市场的惩罚递增机制对决策的影响也更大但需要引入辅助0-1变量把分段线性函数线性化增加了求解复杂度。我后面代码里选用的是“免费配额阶梯碳价”方案。需求响应成本不同类型的响应计算方式不一样。可中断负荷用户允许调度中心在特定时段切除部分负荷每切1千瓦时给一笔补偿金补偿单价一般高于正常电价因为影响了用户用电体验。可转移负荷用户把一部分负荷从一个时段移到另一个时段大多数场景下用户侧不产生直接补偿成本只是用电时间变了。但也存在激励型合约下按转移电量给奖励的情况我按市场常见的“容量补偿电量补偿”来建模。价格型需求响应负荷对电价的敏感度通过弹性矩阵体现电价高时用户自发少用电。这一块通常是改变负荷曲线后再进功率平衡约束成本端不单独体现而是通过购电成本变化间接反映。弃风弃光惩罚用一个较大的系数乘以弃风弃光电量确保模型只在迫不得已时才削减新能源出力。2.3 约束条件不止是等式和不等式约束条件决定了模型的物理可行性我按模块拆开列一下每个模块都有必须注意的细节。功率平衡约束微网/虚拟电厂内部发电、储能放电、购电、新能源出力之和等于负荷需求、储能充电、售电、弃风弃光之和。这个约束每天24个时段每个时段一条是最核心的等式约束。注意可中断负荷和可转移负荷要体现在负荷侧也就是说等式里的“负荷”是经过需求响应调整后的净负荷。燃气轮机约束出力上下限、爬坡约束、最小开停机时间约束、启停状态与出力之间的耦合约束。爬坡约束是很多新手容易漏掉的调度结果里燃气轮机出力从一个时段到另一个时段跳变非常大在物理上根本实现不了。储能约束充放电功率上下限、SOC递推关系、SOC上下限、充放电状态互斥、调度周期始末SOC一致。最后一条“始末SOC一致”很重要因为日前调度的假设是每天的运行是周期性的如果始末SOC不一致连续多日调度会出现电量累积偏移。有些文献不要求始末一致但我建议加上这更符合工程实际。联络线约束购电/售电功率上限以及前面提到的购售互斥。碳约束实际碳排放量等于燃气轮机出力乘以排放因子再累加碳交易量等于实际排放量减去免费配额然后按阶梯碳价线性化计算交易成本。需求响应约束可中断负荷每个时段中断量不超过该时段可中断容量的上限一个调度周期内总中断次数有上限中断持续时段数有限制避免频繁启停对用户生产造成过大影响。可转移负荷转移前后的总用电量保持不变这是一个全局等式约束非常关键每个时段的转移量不超过该时段可转移容量上限转移后的负荷曲线要用原负荷加上转移量来重新构建。价格型需求响应调整后的负荷与原负荷之差和电价变动幅度挂钩一般用需求弹性矩阵表示弹性矩阵由历史数据拟合而来。整套模型写下来混合整数线性规划的规模有多大呢以24时段、1台燃气轮机、1套储能、3类负荷为例决策变量大约300到500个其中0-1变量大概有100到150个。这不算是很大的规模用Matlab的Yalmip工具箱配合Cplex或Gurobi几秒钟到几十秒钟就能解出来非常适合作为研究生课题或者工程方案的基础模型。3. Matlab实现细节从建模到求解的全流程3.1 环境配置与工具选择我的环境是Matlab R2021a建模用Yalmip求解器用Cplex或Gurobi都可以。Cplex对MILP的默认策略比较稳Gurobi在大规模问题上稍微快一点这个问题规模下差别不大。如果你手头只有一个求解器授权不必纠结都能用。安装部分这里不多展开只说一个坑Yalmip是从GitHub下载的matlab工具箱需要加到Matlab路径里Cplex和Gurobi安装后Matlab要能识别到它们的接口通常是求解器安装目录下的matlab文件夹要加进路径。装完之后在Matlab命令行输入yalmiptest能看到所有求解器的状态确保Cplex或Gurobi后面显示OK。3.2 代码结构和核心模块我的代码习惯是分五个模块逻辑会更清晰数据入参负荷、风电光伏预测、分时电价、机组参数、储能参数、碳配额、碳价、需求响应参数。变量定义用Yalmip的sdpvar定义连续变量用binvar定义0-1变量。约束搭建把所有约束逐个写进一个Constraints集合。目标函数按上面的四项成本加惩罚项写一个表达式。求解与结果输出调用optimize求解把结果存成结构体绘制功率平衡图、SOC曲线、碳排放累计图。下面给一个核心模型的骨架代码方便你直接对照理解。注意这不是完整能跑的程序变量定义和参数需要按你自己的数据补全。%% 定义变量 % n: 时段数, 24 P_gt sdpvar(1, n, full); % 燃气轮机出力 u_gt binvar(1, n, full); % 燃气轮机启停 P_ch sdpvar(1, n, full); % 储能充电 P_dis sdpvar(1, n, full); % 储能放电 u_st binvar(1, n, full); % 储能充放电状态, 1为放电 P_buy sdpvar(1, n, full); % 购电 P_sell sdpvar(1, n, full); % 售电 u_grid binvar(1, n, full); % 购售互斥状态, 1为购电 P_il sdpvar(1, n, full); % 可中断负荷量 u_il binvar(1, n, full); % 可中断状态 P_shift sdpvar(1, n, full); % 可转移负荷变化量, 正为转入, 负为转出 E_carbon_buy sdpvar(1, 1, full); % 碳配额购买量 %% 目标函数 F_fuel sum(c_fuel .* P_gt); % 这里用了线性成本系数, 实际可用分段线性 F_grid sum(price_buy .* P_buy - price_sell .* P_sell); F_carbon carbon_price * E_carbon_buy; % 阶梯碳价需要额外线性化 F_dr sum(price_il .* P_il); F_curtail big_M * (sum(P_wind_forecast - P_wind_use) sum(P_pv_forecast - P_pv_use)); F F_fuel F_grid F_carbon F_dr F_curtail; %% 约束 C []; % 功率平衡: 发电放电购电新能源 负荷响应后 充电 售电 for t 1:n C [C, P_gt(t) P_dis(t) P_buy(t) P_wind_use(t) P_pv_use(t) ... P_load(t) P_il(t) P_shift(t) P_ch(t) P_sell(t)]; end % 燃气轮机 C [C, P_min .* u_gt P_gt P_max .* u_gt]; C [C, -P_ramp P_gt(2:end) - P_gt(1:end-1) P_ramp]; % 储能 C [C, 0 P_ch P_ch_max .* (1 - u_st)]; C [C, 0 P_dis P_dis_max .* u_st]; for t 2:n C [C, SOC(t) SOC(t-1) eta_ch * P_ch(t) - P_dis(t) / eta_dis]; end C [C, SOC(1) SOC(24)]; % 日始日末SOC一致 % 购售互斥 C [C, P_buy P_grid_max .* u_grid]; C [C, P_sell P_grid_max .* (1 - u_grid)]; % 可中断负荷 C [C, 0 P_il P_il_max .* u_il]; C [C, sum(u_il) N_il_max]; % 可转移负荷: 总转移电量为0 C [C, sum(P_shift) 0]; C [C, -P_shift_max P_shift P_shift_max]; % 碳约束 E_total sum(P_gt) * emission_factor; C [C, E_carbon_buy E_total - E_allowance];这里要专门说明几处设计意图。可转移负荷我用一个可正可负的变量正代表其他时段负荷转入本时段负代表本时段负荷转出到其他时段。然后加一个全局等式sum(P_shift) 0保证总用电量不变。这个写法比分别定义“转入量”和“转出量”两个变量更简洁本质上等价但对初学者来说可能不太直观。如果你需要分别统计转入、转出电量建议拆成两个非负变量再让转入总量等于转出总量。储能约束里SOC下标从2到nSOC(1)需要单独初始化。按照我的模型SOC(1)是给定初值SOC(24)通过递推计算出来然后强制SOC(1)SOC(24)。如果你想固定SOC初值就将SOC(1)等于某个常数末日再强制相等。注意eta_ch和eta_dis是充电效率和放电效率一般情况下二者不相等不能图省事用一个效率替代否则SOC递推会有能量误差。3.3 为什么选择MILP而不是启发式算法这个模型里既有燃气轮机启停又有储能充放电互斥还有可中断负荷状态本质上是个混合整数线性规划。我在实际做的时候也试过粒子群和遗传算法结果不太理想一是整数变量太多启发式算法对这种高维整数搜索空间很容易陷入局部最优二是每次迭代都要做功率平衡修正处理约束非常繁琐三是对比实验需要跑几十上百次启发式算法的耗时完全不具备可重复性。用Yalmip求解器的好处在于你只需要把目标函数和约束写成数学表达式求解器内部会做分支定界和割平面得到的是全局最优解或者带最优间隙保证的解。对日前调度来说每一天的计算时间在几十秒以内完全满足工程使用要求。这也是现在主流文献和企业实际项目的标准做法。4. 算例设计与结果对比碳交易和需求响应到底带来什么变化4.1 算例参数与场景设置为了让对比结果有意义我设计了一个不算太复杂但覆盖所有功能的小型虚拟电厂。系统内包含一台50MW的燃气轮机一套20MW/80MWh的储能系统风电装机80MW光伏装机40MW基础负荷峰值大约140MW联络线最大购售电功率60MW。典型日的风电光伏预测用一条晚上风大、白天光伏高的曲线分时电价按峰、平、谷三段设置尖峰时段电价是谷时的3倍。碳排放方面燃气轮机排放因子取0.2t/MWh免费配额按基准线法给出阶梯碳价设为三段超排量在50吨以内碳价100元/吨50到100吨部分150元/吨超过100吨部分200元/吨。可中断负荷容量取基础负荷的5%补偿单价为600元/MWh可转移负荷容量取基础负荷的8%转移上限是每小时基础负荷的3%。为了提高对比度我设置了三组方案方案A不考虑碳交易、不考虑需求响应就是传统经济调度。方案B只加碳交易需求响应不启用。方案C同时加碳交易和多种需求响应也就是完整模型。4.2 输出结果与关键指标对比三个方案跑完之后我用四个指标来评估总运行成本、碳排放量、负荷峰谷差、新能源消纳率。总运行成本这组数据最容易看出差异。方案A因为燃气轮机可以按经济性自由出力高峰时段电价贵就多发自家电总成本看似低但这是没算碳成本的结果。方案B把碳交易成本算进去之后燃气轮机出力明显被抑制高峰时段更多依赖购电总成本比方案A高了大约6%——这个增量不是白白多花的钱而是把碳排放的外部性内部化之后系统为低碳支付的成本。方案C加入需求响应后高峰时段可中断负荷被切掉一部分可转移负荷被挪到凌晨风电大发时段负荷峰谷差缩小了将近22%购电成本下降燃气轮机在高峰时的压力也减轻了总成本反而比方案B还低一些。碳排放量的变化更直观。方案A里燃气轮机满负荷运行的时间最多碳排放量处于高位。方案B在碳价约束下燃气轮机出力下调碳排放量下降约18%。方案C进一步通过负荷转移让燃气轮机避免了部分高峰爬坡碳排放量下降约25%。再看看新能源消纳率。方案A里零成本的风电光伏本来应该全额消纳但因为没有需求响应凌晨低谷时段负荷太低储能又满了只能弃风。方案C里可转移负荷把一部分白天高峰负荷挪到凌晨正好填补了这个低谷缺口弃风率从方案A的9.6%降到了2.1%。这个变化对实际项目来说意义非常直接新能源消纳率每提升一个点对应的都是收入增加和政策考核指标的改善。4.3 结果验证怎么判断你的模型算对了这是我在帮别人检查代码时最常遇到的一个问题模型能跑出结果但没人知道结果对不对或者说不知道有没有违反物理约束。我的习惯是在求解结束后做三步验证。第一步检查功率平衡。把每时段的发电、购电、储能放电、新能源出力加总再减去负荷、充电、售电误差应该在一个很小的阈值内比如1e-6。这个检查可以直接在Matlab里用norm函数算残差。第二步检查SOC。把整个调度周期的SOC画出来看是否在上下限内看始末是否一致看充放电功率和SOC变化是否吻合。储能部分很容易出现“SOC跳变”这种物理上不可能的调度结果一旦发现优先检查效率系数和SOC递推公式的符号。第三步检查需求响应的全局约束。可转移负荷是否满足总电量不变可中断负荷是否超容量上限这些都可以在求解结束后直接对变量取值做校验。如果结果恰好卡在边界上说明约束起作用的时段和预期一致这个方案在经济学上的解释也成立。我在实际研究中发现很多错误其实出在数据上而不是模型上。比如峰谷电价时段设置与负荷曲线对不上导致模型在低谷时段大量购电、高峰时段反而售电再比如排放因子的单位用错kg和吨之间差了一千倍碳成本计算完全失真。建议所有单位统一用MWh和元碳排量用吨这样算出来的数值量级比较符合常规认知。5. 实操中踩过的坑与排查经验5.1 阶梯碳价的线性化处理阶梯碳价是分段线性函数直接写进目标函数会变成非线性Yalmip虽然能处理部分非线性但求解效率会下降而且Cplex对非线性模型的求解能力有限。这里需要显式线性化。最常用的办法是引入分段区间指示变量和连续辅助变量。比如把超排量分成三档每档的配额区间是上限减去下限每档的碳价不同再确保各档按顺序填满。这个线性化模型需要额外的0-1变量但规模不大。如果不做阶梯碳价直接用单一碳价模型会简单很多能效上差不了太多。我的建议是如果你的研究重点是碳交易机制本身的对比可以做阶梯碳价如果只是需要一个碳约束用单一碳价也够后期再加装。5.2 可转移负荷的建模陷阱可转移负荷是我调试时间最长的一部分。最常犯的错误是把sum(P_shift) 0这个全局约束漏掉或者写成每个时段转移前后自己平衡。漏掉全局约束的后果是模型会“凭空创造”电量低谷时段大量转入负荷高峰时段大量转出总用电量增加了目标函数里购电成本又因为负荷平移降低了就出现一个看似完美但物理上不可能的优化结果。另一个坑是没限制可转移负荷的响应范围。如果只设置每个时段转移量上限模型会把负荷在多个时段之间来回倒腾可能同一批负荷被转移了两次以上这在真实需求响应场景里是不允许的。实际做法是对参与转移的负荷类型做标记可转移负荷通常对应工业生产流程或电动车充电这类有明确用电窗口的用电设备同一设备只能转移一次。想精确建模的话需要给每类设备单独定义转移窗口和转移次数上限但这会让模型变大。折中的办法是把转移次数约束近似为限制每个时段转移量的单调性或者直接在统计可转移容量时按保守值给。5.3 求解器报Infeasible的排查思路约束一多不可避免会遇到Cplex报“Infeasible”的情况。我最开始调试时习惯一股脑全查效率极低。后面总结出一个比较高效的排查流程。第一步先把所有需求响应相关的约束注释掉只保留最基础的功率平衡、机组、储能。如果这个模型能解说明基础模型没问题问题出在需求响应模块。第二步把需求响应约束一条条加回去每加一条就跑一次看到底哪条约束导致无解。通常问题出在可转移负荷的容量设置太小或者可中断负荷占比设置太大把功率平衡逼死了。比如可转移负荷把高峰负荷全部挪到低谷但低谷时段联络线购电上限不够就会无解。第三步检查约束的量纲和符号。我踩过最蠢的坑是爬坡约束里P_gt(2:end) - P_gt(1:end-1)写成了P_gt(1:end-1) - P_gt(2:end)方向反了导致模型强制要求机组每个时段爬坡到上限自然无解。这种问题用眼睛很难看出来所以建议先在Matlab里单独打印几个约束表达式的值对比下量级是否合理。5.4 求解速度与数值稳定性模型规模不大但0-1变量如果处理不好求解时间会从几秒暴涨到几分钟。这里有几个经验性的优化手段。第一所有大M约束里的M值不要取太大能取边界值的1.2倍就够了。比如购售互斥约束里购电功率上限是60MW那么M取70就够不要写1e6。M太大不会改最优解但会显著拉长分支定界的收敛时间。第二连续变量的上下界要尽量紧凑。储能充放电功率上限如果给了100MW但实际只有20MW求解器在分支过程中会探索很多不可行区域。第三Yalmip默认会把所有约束统一交给求解器但你可以通过sdpsettings(solver,cplex)指定求解器并开verbose查看求解日志。如果求解时间明显变长看日志里是哪个节点耗时多再针对性处理。5.5 Yalmip调试技巧调试Yalmip模型有一个非常实用的技巧先用assign给变量赋值然后用check检查所有约束是否满足。具体做法是求解完成之后把变量的求解结果assign回去再运行check(Constraints)Yalmip会输出每个约束的最大残差。如果某个约束残差很大基本就是这个约束写错了。还有一个技巧是把目标函数拆开看。我先分别计算燃料成本、购电成本、碳成本、需求响应补偿成本各是多少看每一项是否符合直觉。比如碳成本如果为零先检查是不是免费配额给得太多了需求响应补偿成本如果为零检查是不是可中断负荷状态变量压根没有被置1。这种拆解式的检查比直接看总成本更容易定位问题。结语这个项目做下来我最大的体会是调度模型的难度不在求解而在建模时对物理过程和数据细节的理解。碳交易不是加一个碳价系数那么简单需求响应也不是一句“负荷可以调”就完事。它们都需要认真考虑实际的物理限制、市场规则和数据可信度才能得到真正可用的调度方案。如果你接下来要扩展这个模型我建议可以往两个方向走。一个方向是考虑不确定性把风电光伏预测误差建模成随机场景做两阶段随机规划或分布鲁棒优化这样调度方案对实际运行的适应性会更强。另一个方向是引入碳捕集设备和绿氢等新元素让“低碳”从被动约束变成主动资源。这些在目前的研究和工程实践中都非常热门模型框架可以复用现在的这套基础只是变量和约束再往里加一层。希望这篇文章能帮你少走一些弯路把精力花在真正值得研究的问题上。